summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNic Eigel <nic@eigel.ch>2024-01-14 15:18:12 +0100
committerNic Eigel <nic@eigel.ch>2024-01-14 15:18:12 +0100
commit7a201c3b885c5d23bf0fd0f3da32379a49b30c38 (patch)
tree13f35c4761087b0e6adce39153be5ca03c5c846b
parent2be9142ac5f944fbc03186b22ca67e6020187c92 (diff)
downloadwallet-core-7a201c3b885c5d23bf0fd0f3da32379a49b30c38.tar.gz
wallet-core-7a201c3b885c5d23bf0fd0f3da32379a49b30c38.tar.bz2
wallet-core-7a201c3b885c5d23bf0fd0f3da32379a49b30c38.zip
adding auditor-backoffice-ui
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts3
-rw-r--r--packages/auditor-backoffice-ui/.gitignore6
-rw-r--r--packages/auditor-backoffice-ui/DESIGN.md195
-rw-r--r--packages/auditor-backoffice-ui/Makefile35
-rw-r--r--packages/auditor-backoffice-ui/README.md64
-rwxr-xr-xpackages/auditor-backoffice-ui/build.mjs28
-rwxr-xr-xpackages/auditor-backoffice-ui/contrib/po2ts42
-rw-r--r--packages/auditor-backoffice-ui/copyleft-header.js15
-rwxr-xr-xpackages/auditor-backoffice-ui/dev.mjs40
-rw-r--r--packages/auditor-backoffice-ui/package.json83
-rw-r--r--packages/auditor-backoffice-ui/preact.config.js70
-rw-r--r--packages/auditor-backoffice-ui/preact.single-config.js62
-rw-r--r--packages/auditor-backoffice-ui/remove-link-stylesheet.sh8
-rw-r--r--packages/auditor-backoffice-ui/src/AdminRoutes.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/Application.tsx165
-rw-r--r--packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx175
-rw-r--r--packages/auditor-backoffice-ui/src/InstanceRoutes.tsx800
-rw-r--r--packages/auditor-backoffice-ui/src/assets/empty.pngbin0 -> 2785 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.pngbin0 -> 14058 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.pngbin0 -> 51484 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.pngbin0 -> 12746 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.pngbin0 -> 626 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.pngbin0 -> 1487 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg48
-rw-r--r--packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.pngbin0 -> 9050 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/auditor-backoffice-ui/src/assets/logo.jpegbin0 -> 39336 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx55
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/QR.tsx49
-rw-r--r--packages/auditor-backoffice-ui/src/components/exception/loading.tsx48
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx109
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/Input.tsx116
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputArray.tsx139
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx91
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx67
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputDate.tsx164
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx86
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputImage.tsx122
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx60
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx47
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx397
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx204
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx94
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx162
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputStock.tsx224
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputTab.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx147
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx91
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx116
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx59
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/TextField.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/useField.tsx92
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx41
-rw-r--r--packages/auditor-backoffice-ui/src/components/index.stories.ts17
-rw-r--r--packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx92
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx284
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/index.tsx269
-rw-r--r--packages/auditor-backoffice-ui/src/components/modal/index.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx62
-rw-r--r--packages/auditor-backoffice-ui/src/components/notifications/index.tsx65
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx349
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx55
-rw-r--r--packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx211
-rw-r--r--packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx62
-rw-r--r--packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx127
-rw-r--r--packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx215
-rw-r--r--packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx178
-rw-r--r--packages/auditor-backoffice-ui/src/components/product/ProductList.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/context/backend.test.ts163
-rw-r--r--packages/auditor-backoffice-ui/src/context/backend.ts69
-rw-r--r--packages/auditor-backoffice-ui/src/context/config.ts32
-rw-r--r--packages/auditor-backoffice-ui/src/context/instance.ts36
-rw-r--r--packages/auditor-backoffice-ui/src/custom.d.ts42
-rw-r--r--packages/auditor-backoffice-ui/src/declaration.d.ts1830
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/async.ts77
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/backend.ts477
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/bank.ts217
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts161
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/index.ts151
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/instance.test.ts741
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/instance.ts313
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/listener.ts85
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/notifications.ts56
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/order.test.ts587
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/order.ts289
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/otp.ts223
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/product.test.ts362
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/product.ts177
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/reserve.test.ts448
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/reserves.ts181
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/templates.ts266
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/testing.tsx180
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/transfer.test.ts254
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/transfer.ts188
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/urls.ts303
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/useSettings.ts73
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/webhooks.ts178
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/de.po2742
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/en.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/es.po2854
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/fr.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/it.po2742
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/poheader27
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings-prelude19
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings.ts9655
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/sv.po2741
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot2726
-rw-r--r--packages/auditor-backoffice-ui/src/index.html45
-rw-r--r--packages/auditor-backoffice-ui/src/index.tsx24
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx257
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx74
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx82
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx287
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx110
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx140
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx173
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx65
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx385
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx195
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx60
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx151
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx83
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx87
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts19
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx58
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx208
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx63
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx705
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx135
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx770
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx129
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx226
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx417
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx231
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx180
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx104
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx70
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx211
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx171
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx60
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx496
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx150
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx73
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx277
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx120
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx190
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx266
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx126
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx88
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx320
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx171
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx259
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx235
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx152
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx172
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx254
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx143
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx101
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx45
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx93
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx134
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx229
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx26
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx59
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx176
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx183
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx61
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx218
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx109
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx146
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx99
-rw-r--r--packages/auditor-backoffice-ui/src/paths/login/index.tsx202
-rw-r--r--packages/auditor-backoffice-ui/src/paths/notfound/index.tsx34
-rw-r--r--packages/auditor-backoffice-ui/src/paths/settings/index.tsx112
-rw-r--r--packages/auditor-backoffice-ui/src/schemas/index.ts245
-rw-r--r--packages/auditor-backoffice-ui/src/scss/DurationPicker.scss70
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_aside.scss181
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_card.scss69
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss259
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_footer.scss35
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_form.scss71
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_hero-bar.scss55
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_loading.scss51
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_main-section.scss24
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_misc.scss50
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_mixins.scss34
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_modal.scss35
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_nav-bar.scss144
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_table.scss179
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_theme-default.scss136
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_tiles.scss24
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_title-bar.scss50
-rw-r--r--packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttfbin0 -> 43752 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/fonts/nunito.css22
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eotbin0 -> 844600 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttfbin0 -> 844380 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woffbin0 -> 404384 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2bin0 -> 283040 bytes
-rw-r--r--packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css15109
-rw-r--r--packages/auditor-backoffice-ui/src/scss/libs/_all.scss29
-rw-r--r--packages/auditor-backoffice-ui/src/scss/main.scss195
-rw-r--r--packages/auditor-backoffice-ui/src/scss/toggle.scss51
-rw-r--r--packages/auditor-backoffice-ui/src/stories.test.ts44
-rw-r--r--packages/auditor-backoffice-ui/src/stories.tsx48
-rw-r--r--packages/auditor-backoffice-ui/src/sw.js25
-rw-r--r--packages/auditor-backoffice-ui/src/utils/amount.ts71
-rw-r--r--packages/auditor-backoffice-ui/src/utils/constants.ts197
-rw-r--r--packages/auditor-backoffice-ui/src/utils/crypto.ts61
-rw-r--r--packages/auditor-backoffice-ui/src/utils/regex.test.ts88
-rw-r--r--packages/auditor-backoffice-ui/src/utils/table.ts57
-rw-r--r--packages/auditor-backoffice-ui/src/utils/types.ts31
-rwxr-xr-xpackages/auditor-backoffice-ui/test.mjs31
-rw-r--r--packages/auditor-backoffice-ui/tsconfig.json58
283 files changed, 79533 insertions, 1 deletions
diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts
index 3f62258c6..baf3445c4 100644
--- a/packages/aml-backoffice-ui/src/context/config.ts
+++ b/packages/aml-backoffice-ui/src/context/config.ts
@@ -60,7 +60,8 @@ export const ExchangeApiProvider = ({
}): VNode => {
const [checked, setChecked] = useState<ConfigResult>()
const { i18n } = useTranslationContext();
- const url = new URL(baseUrl)
+ //const url = new URL(baseUrl)
+ const url = new URL("http://localhost:8081");
const api = new TalerExchangeHttpClient(url.href, new BrowserHttpLib())
useEffect(() => {
api.getConfig()
diff --git a/packages/auditor-backoffice-ui/.gitignore b/packages/auditor-backoffice-ui/.gitignore
new file mode 100644
index 000000000..df149101c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/.gitignore
@@ -0,0 +1,6 @@
+/build
+/size-plugin.json
+/storybook-static
+/docs
+/single
+/coverage
diff --git a/packages/auditor-backoffice-ui/DESIGN.md b/packages/auditor-backoffice-ui/DESIGN.md
new file mode 100644
index 000000000..d6252ccdc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/DESIGN.md
@@ -0,0 +1,195 @@
+# Page internal routing
+
+* The SPA is loaded from the BACKOFFICE_URL
+
+* The view to be rendered is decided by the URL fragment
+
+* Query parameters that may affect routing
+
+ - instance: use from the default instance to mimic another instance management
+
+* The user must provide BACKEND_URL or BACKOFFICE_URL will use as default
+
+* Token for querying the backend will be saved in localStorage under
+ backend-token-${name}
+
+# HTTP queries to the backend
+
+HTTP queries will have 4 states:
+
+* loading: request did not end yet. data and error are undefined
+
+* ok: data has information, http response status == 200
+
+* clientError: http response status is between 400 and 499
+
+ - notfound: http status 404
+
+ - unauthorized: http status 401
+
+* serverError: http response status is grater than 500
+
+There are categories of queries:
+
+ * sync: getting information for the page rendering
+
+ * async: performing an CRUD operation
+
+## Loading the page information (sync)
+
+In this scenario, a failed request will make the app flow to break.
+
+When receiving an not found error a generic not found page will be shown. If the
+BACKEND_URL points to a default instance it should send the user to create the
+instance.
+
+When receiving an unauthorized error, the user should be prompted with a login form.
+
+When receiving an another error (400 < http status < 600), the login form should
+be shown with an error message using the hint from the backend.
+
+On other unexpected error (like network error), the login form should be shown
+with an error message.
+
+## CRUD operation (async)
+
+In this scenario, a failed request does not break the flow but a message will be
+prompted.
+
+# Forms
+
+All the input components should be placed in the folder `src/components/from`.
+
+The core concepts are:
+
+ * <FormProvider<T> /> places instead of <form /> it should be mapped to an
+ object of type T
+
+ * <Input /> an others: defines UI, create <input /> DOM controls and access the
+ form with useField()
+
+To use it you will need a state somewhere with the object holding all the form
+information.
+
+```
+const [state, setState] = useState({ name: '', age: 11 })
+```
+
+Optionally an error object an be built with the error messages
+
+```
+const errors = {
+ field1: undefined,
+ field2: 'should be greater than 18',
+}
+```
+
+These 3 elements are used to setup the FormProvider
+
+```
+<FormProvider errors={errors} object={state} valueHandler={setState}>
+...inputs
+</FormProvider>
+```
+
+Inputs should handle UI rendering and use `useField(name)` to get:
+
+ * error: the field has been modified and the value is not correct
+ * required: the field need to be corrected
+ * value: the current value of the object
+ * initial: original value before change
+ * onChange: function to update the current field
+
+Also, every input must be ready to receive these properties
+
+ * name: property of the form object being manipulated
+ * label: how the name of the property will be shown in the UI
+ * placeholder: optional, inplace text when there is no value yet
+ * readonly: default to false, will prevent change the value
+ * help: optional, example text below the input text to help the user
+ * tooltip: optional, will add a (i) with a popup to describe the field
+
+
+# Custom Hooks
+
+All the general purpose hooks should be placed in folder `src/hooks` and tests
+under `tests/hooks`. Starts with the `use` word.
+
+# Contexts
+
+All the contexts should be placed in the folder `src/context` as a function.
+Should expose provider as a component `<XxxContextProvider />` and consumer as a
+hook function `useXxxContext()` (where XXX is the name)
+
+# Components
+
+Type of components:
+
+ * main entry point: src/index.tsx, mostly initialization
+
+ * routing: in the `src` folder, deciding who is going to take the work. That's
+ when the page is loading but also create navigation handlers
+
+ * pages: in the `paths` folder, setup page information (like querying the
+ backend for the list of things), handlers for CRUD events, delegated routing
+ to parent and UI to children.
+
+Some other guidelines:
+
+ * Hooks over classes are preferred
+
+ * Components that are ready to be reused on any place should be in
+ `src/components` folder
+
+ * Since one of the build targets is a single bundle with all the pages, we are
+ avoiding route based code splitting
+ https://github.com/preactjs/preact-cli#route-based-code-splitting
+
+
+# Testing
+
+Every components should have examples using storybook (xxx.stories.tsx). There
+is an automated test that check that every example can be rendered so we make
+sure that we do not add a regression.
+
+Every hook should have examples under `tests/hooks` with common usage trying to
+follow this structure:
+
+ * (Given) set some context of the initial condition
+
+ * (When) some action to be tested. May be the initialization of a hook or an
+ action associated with it
+
+ * (Then) a particular set of observable consequences should be expected
+
+# Accessibility
+
+Pages and components should be built with accessibility in mind.
+
+https://github.com/nickcolley/jest-axe
+https://orkhanhuseyn.medium.com/accessibility-testing-in-react-with-jest-axe-e08c2a3f3289
+http://accesibilidadweb.dlsi.ua.es/?menu=jaws
+https://webaim.org/projects/screenreadersurvey8/#intro
+https://www.gov.uk/service-manual/technology/testing-with-assistive-technologies#how-to-test
+https://es.reactjs.org/docs/accessibility.html
+
+# Internationalization
+
+Every non translated message should be written in English and wrapped into:
+
+ * i18n function from useTranslator() hook
+ * <Translate /> component
+
+Makefile has a i18n that will parse source files and update the po template.
+When *.po are updated, running the i18n target will create the strings.ts that
+the application will use in runtime.
+
+# Documentation Conventions
+
+* labels
+ * begin w/ a capital letter
+ * acronyms (e.g., "URL") are upper case
+* tooltips
+ * begin w/ a lower case letter
+ * do not end w/ punctuation (period)
+ * avoid leading article ("a", "an", "the")
diff --git a/packages/auditor-backoffice-ui/Makefile b/packages/auditor-backoffice-ui/Makefile
new file mode 100644
index 000000000..57b3e0cb5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/Makefile
@@ -0,0 +1,35 @@
+# This Makefile has been placed in the public domain
+
+ifeq ($(TOPLEVEL), yes)
+ $(info top-level build)
+ -include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
+else
+ $(info package-level build)
+ -include ../../.config.mk
+ -include .config.mk
+endif
+
+$(info prefix is $(prefix))
+
+.PHONY: all
+all:
+ @echo run \'make install\' to install
+
+spa_dir=$(DESTDIR)$(prefix)/share/taler/auditor-backoffice
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice...
+ pnpm run build
+
+.PHONY: install-nodeps
+install-nodeps:
+ (cd dist/prod && find . -type f -exec install -D "{}" "$(spa_dir)/{}" \;)
+
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/auditor-backoffice-ui/README.md b/packages/auditor-backoffice-ui/README.md
new file mode 100644
index 000000000..b10fa6a94
--- /dev/null
+++ b/packages/auditor-backoffice-ui/README.md
@@ -0,0 +1,64 @@
+## AUditor Admin Frontend
+
+Auditor Admin Frontend is a Single Page Application (SPA) that connects with a running Auditor Backend and lets you audit the exchange.
+
+## System requirements
+
+- Node: v16.15.0
+- pnpm: 7.14.2
+- make
+
+## Compiling from source
+
+Run `pnpm install --frozen-lockfile --filter @gnu-taler/auditor-backoffice...` to install all the nodejs dependencies.
+
+Then the command `pnpm build` create the distribution in the `dist` folder.
+
+By default the installation prefix will be `/usr/local/share/taler/auditor-backoffice/` but it can be overridden by `--prefix` in the configuration process:
+
+```shell
+./configure --prefix=/another/directory
+```
+
+To install run `make install`
+
+## Running develop
+
+To run a development server run:
+
+```shell
+./dev.mjs
+```
+
+This should start a watch process that will reload the server every time that a file is saved.
+
+The application need to connect to a auditor-backend properly configured to run.
+
+## Building for deploy
+
+To build and deploy the SPA in your local server run the install script:
+
+```shell
+make install
+```
+
+## Runtime dependencies
+
+* preact: Fast 3kB alternative to React with the same modern API
+
+* preact-router: URL component router for Preact
+
+* SWR: React Hooks library for data fetching (stale-while-revalidate)
+
+* Yup: schema builder for value parsing and validation (to be deprecated)
+
+* Date-fns: library for manipulating javascript date
+
+* qrcode-generator: simplest qr implementation based on JIS X 0510:1999
+
+* @gnu-taler/taler-util: types and tooling
+
+* history: manage the history stack, navigate, and persist state between sessions
+
+* jed: gettext like library for internationalization
+
diff --git a/packages/auditor-backoffice-ui/build.mjs b/packages/auditor-backoffice-ui/build.mjs
new file mode 100755
index 000000000..b6d6e5127
--- /dev/null
+++ b/packages/auditor-backoffice-ui/build.mjs
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+/*
+ 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/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/prod",
+ css: "sass",
+});
diff --git a/packages/auditor-backoffice-ui/contrib/po2ts b/packages/auditor-backoffice-ui/contrib/po2ts
new file mode 100755
index 000000000..d32e922ba
--- /dev/null
+++ b/packages/auditor-backoffice-ui/contrib/po2ts
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Convert a <lang>.po file into a JavaScript / TypeScript expression.
+ */
+
+const po2json = require("po2json");
+
+const filename = process.argv[2];
+
+if (!filename) {
+ console.error("error: missing filename");
+ process.exit(1);
+}
+
+const m = filename.match(/([a-zA-Z0-9-_]+).po/);
+
+if (!m) {
+ console.error("error: unexpected filename (expected <lang>.po)");
+ process.exit(1);
+}
+
+const lang = m[1];
+const pojson = po2json.parseFileSync(filename, { format: "jed1.x", fuzzy: true });
+const s =
+ "strings['" + lang + "'] = " + JSON.stringify(pojson, null, " ") + ";\n";
+console.log(s);
diff --git a/packages/auditor-backoffice-ui/copyleft-header.js b/packages/auditor-backoffice-ui/copyleft-header.js
new file mode 100644
index 000000000..2589fdc92
--- /dev/null
+++ b/packages/auditor-backoffice-ui/copyleft-header.js
@@ -0,0 +1,15 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
diff --git a/packages/auditor-backoffice-ui/dev.mjs b/packages/auditor-backoffice-ui/dev.mjs
new file mode 100755
index 000000000..14d5737de
--- /dev/null
+++ b/packages/auditor-backoffice-ui/dev.mjs
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
+
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ css: "sass",
+ destination: "./dist/dev",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
new file mode 100644
index 000000000..1fe7332c0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/package.json
@@ -0,0 +1,83 @@
+{
+ "private": true,
+ "name": "@gnu-taler/auditor-backoffice-ui",
+ "version": "0.9.3-dev.27",
+ "license": "AGPL-3.0-or-later",
+ "type": "module",
+ "scripts": {
+ "build": "./build.mjs",
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
+ "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/**/*.test.js' 'dist/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "i18n:extract": "pogen extract",
+ "i18n:merge": "pogen merge",
+ "i18n:emit": "pogen emit",
+ "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "typedoc": "typedoc --out dist/typedoc ./src/",
+ "pretty": "prettier --write src"
+ },
+ "eslintConfig": {
+ "plugins": [
+ "header"
+ ],
+ "rules": {
+ "header/header": [
+ 2,
+ "copyleft-header.js"
+ ]
+ },
+ "extends": [
+ "prettier"
+ ]
+ },
+ "dependencies": {
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "history": "4.10.1",
+ "jed": "1.1.1",
+ "preact": "10.11.3",
+ "preact-router": "3.2.1",
+ "qrcode-generator": "1.4.4",
+ "swr": "2.2.2",
+ "yup": "^0.32.9"
+ },
+ "devDependencies": {
+ "@creativebulma/bulma-tooltip": "^1.2.0",
+ "@gnu-taler/pogen": "^0.0.5",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^8.2.3",
+ "@types/node": "^18.11.17",
+ "@typescript-eslint/eslint-plugin": "^4.22.0",
+ "@typescript-eslint/parser": "^4.22.0",
+ "base64-inline-loader": "^1.1.1",
+ "bulma": "^0.9.2",
+ "bulma-checkbox": "^1.1.1",
+ "bulma-radio": "^1.1.1",
+ "bulma-responsive-tables": "^1.2.3",
+ "bulma-switch-control": "^1.1.1",
+ "bulma-timeline": "^3.0.4",
+ "bulma-upload-control": "^1.2.0",
+ "chai": "^4.3.6",
+ "dotenv": "^8.2.0",
+ "eslint": "^7.25.0",
+ "eslint-config-preact": "^1.1.4",
+ "eslint-plugin-header": "^3.1.1",
+ "html-webpack-inline-chunk-plugin": "^1.1.1",
+ "html-webpack-inline-source-plugin": "0.0.10",
+ "html-webpack-skip-assets-plugin": "^1.0.1",
+ "inline-chunk-html-plugin": "^1.1.1",
+ "mocha": "^9.2.0",
+ "preact-render-to-string": "^5.2.6",
+ "sass": "1.56.1",
+ "source-map-support": "^0.5.21",
+ "typedoc": "^0.25.4",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "taler-auditor-backoffice"
+ }
+}
diff --git a/packages/auditor-backoffice-ui/preact.config.js b/packages/auditor-backoffice-ui/preact.config.js
new file mode 100644
index 000000000..9b65d3ec7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/preact.config.js
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { DefinePlugin } from 'webpack';
+
+import pack from './package.json';
+import * as cp from 'child_process';
+
+const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
+
+export default {
+ webpack(config, env, helpers) {
+ // ensure that process.env will not be undefined on runtime
+ config.node.process = 'mock'
+
+ // add __VERSION__ to be use in the html
+ config.plugins.push(
+ new DefinePlugin({
+ 'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`) ,
+ }),
+ );
+
+ // suddenly getting out of memory error from build process, error below [1]
+ // FIXME: remove preact-cli, use rollup
+ let { index } = helpers.getPluginsByName(config, 'WebpackFixStyleOnlyEntriesPlugin')[0]
+ config.plugins.splice(index, 1)
+ }
+}
+
+
+
+/* [1] from this error decided to remove plugin 'webpack-fix-style-only-entries
+ leaving this error for future reference
+
+
+<--- Last few GCs --->
+
+[32479:0x2e01870] 19969 ms: Mark-sweep 1869.4 (1950.2) -> 1443.1 (1504.1) MB, 497.5 / 0.0 ms (average mu = 0.631, current mu = 0.455) allocation failure scavenge might not succeed
+[32479:0x2e01870] 21907 ms: Mark-sweep 2016.9 (2077.9) -> 1628.6 (1681.4) MB, 1596.0 / 0.0 ms (average mu = 0.354, current mu = 0.176) allocation failure scavenge might not succeed
+
+<--- JS stacktrace --->
+
+==== JS stack trace =========================================
+
+ 0: ExitFrame [pc: 0x13cf099]
+Security context: 0x2f4ca66c08d1 <JSObject>
+ 1: /* anonymous * / [0x35d05555b4b9] [...path/merchant-backoffice/node_modules/.pnpm/webpack-fix-style-only-entries@0.5.2/node_modules/webpack-fix-style-only-entries/index.js:~80] [pc=0x2145e699d1a4](this=0x1149465410e9 <GlobalObject Object map = 0xff481b5b5f9>,0x047e52e36a49 <Dependency map = 0x1ed1fe41cd19>)
+ 2: arguments adaptor frame: 3...
+
+FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
+
+*/ \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/preact.single-config.js b/packages/auditor-backoffice-ui/preact.single-config.js
new file mode 100644
index 000000000..849269d6e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/preact.single-config.js
@@ -0,0 +1,62 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import defaultConfig from './preact.config'
+
+export default {
+ webpack(config, env, helpers, options) {
+ defaultConfig.webpack(config, env, helpers, options)
+
+ //1. check no file is under /routers or /component/{routers,async} to prevent async components
+ // https://github.com/preactjs/preact-cli#route-based-code-splitting
+
+ //2. remove devtools to prevent sourcemaps
+ config.devtool = false
+
+ //3. change assetLoader to load assets inline
+ const loaders = helpers.getLoaders(config)
+ const assetsLoader = loaders.find(lo => lo.rule.test.test('something.woff'))
+ if (assetsLoader) {
+ assetsLoader.rule.use = 'base64-inline-loader'
+ assetsLoader.rule.loader = undefined
+ }
+
+ //4. remove critters
+ //critters remove the css bundle from htmlWebpackPlugin.files.css
+ //for now, pushing all the content into the html is enough
+ const crittersWrapper = helpers.getPluginsByName(config, 'Critters')
+ if (crittersWrapper && crittersWrapper.length > 0) {
+ const [{ index }] = crittersWrapper
+ config.plugins.splice(index, 1)
+ }
+
+ //5. remove favicon from src/assets
+
+ //6. remove performance hints since we now that this is going to be big
+ if (config.performance) {
+ config.performance.hints = false
+ }
+
+ //7. template.html should have a favicon and add js/css content
+
+ //last, after building remove the mysterious link to stylesheet with remove-link-stylesheet.sh
+ }
+}
diff --git a/packages/auditor-backoffice-ui/remove-link-stylesheet.sh b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh
new file mode 100644
index 000000000..fdf8f241c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/remove-link-stylesheet.sh
@@ -0,0 +1,8 @@
+# This script has been placed in the public domain.
+
+FILE=$(ls single/bundle.*.css)
+BUNDLE=${FILE#single}
+grep -q '<link href="'$BUNDLE'" rel="stylesheet">' single/index.html || { echo bundle $BUNDLE not found in index.html; exit 1; }
+echo -n Removing link from index.html ...
+sed 's_<link href="'$BUNDLE'" rel="stylesheet">__' -i single/index.html
+echo done
diff --git a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
new file mode 100644
index 000000000..91dec09b0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
@@ -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/>
+ */
+import { h, VNode } from "preact";
+import { Router, route, Route } from "preact-router";
+import InstanceCreatePage from "./paths/admin/create/index.js";
+import InstanceListPage from "./paths/admin/list/index.js";
+
+export enum AdminPaths {
+ list_instances = "/instances",
+ new_instance = "/instance/new",
+}
+
+export function AdminRoutes(): VNode {
+ return (
+ <Router>
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ />
+
+ <Route
+ path={AdminPaths.new_instance}
+ component={InstanceCreatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ // route(AdminPaths.list_instances);
+ }}
+
+ // onError={(error: any) => {
+ // }}
+ />
+ </Router>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/Application.tsx b/packages/auditor-backoffice-ui/src/Application.tsx
new file mode 100644
index 000000000..e832d3107
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/Application.tsx
@@ -0,0 +1,165 @@
+/*
+ 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)
+ */
+
+import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useMemo } from "preact/hooks";
+import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
+import { Loading } from "./components/exception/loading.js";
+import {
+ NotConnectedAppMenu,
+ NotificationCard
+} from "./components/menu/index.js";
+import {
+ BackendContextProvider
+} from "./context/backend.js";
+import { ConfigContextProvider } from "./context/config.js";
+import { useBackendConfig } from "./hooks/backend.js";
+import { strings } from "./i18n/strings.js";
+
+export function Application(): VNode {
+ return (
+ <BackendContextProvider>
+ <TranslationProvider source={strings}>
+ <ApplicationStatusRoutes />
+ </TranslationProvider>
+ </BackendContextProvider>
+ );
+}
+
+/**
+ * Check connection testing against /config
+ *
+ * @returns
+ */
+function ApplicationStatusRoutes(): VNode {
+ const result = useBackendConfig();
+ const { i18n } = useTranslationContext();
+
+ const { currency, version } = result.ok && result.data
+ ? result.data
+ : { currency: "unknown", version: "unknown" };
+ const ctx = useMemo(() => ({ currency, version }), [currency, version]);
+
+ if (!result.ok) {
+ if (result.loading) return <Loading />;
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ ) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Login" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Checking the /config endpoint got authorization error`,
+ type: "ERROR",
+ description: `The /config endpoint of the backend server should be accesible`,
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ ) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Error" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Could not find /config enpoint on this URL`,
+ type: "ERROR",
+ description: `Check the URL or contact the system administrator.`,
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (result.type === ErrorType.SERVER) {
+ <Fragment>
+ <NotConnectedAppMenu title="Error" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Server response with an error code`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>;
+ }
+ if (result.type === ErrorType.UNREADABLE) {
+ <Fragment>
+ <NotConnectedAppMenu title="Error" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>;
+ }
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Error" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Unexpected Error`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>
+ );
+ }
+
+ const SUPPORTED_VERSION = "5:0:1"
+ if (result.data && !LibtoolVersion.compare(
+ SUPPORTED_VERSION,
+ result.data.version,
+ )?.compatible) {
+ return <Fragment>
+ <NotConnectedAppMenu title="Error" />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Incompatible version`,
+ type: "ERROR",
+ description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`,
+ }}
+ />
+ </Fragment>
+ }
+
+ return (
+ <div class="has-navbar-fixed-top">
+ <ConfigContextProvider value={ctx}>
+ <ApplicationReadyRoutes />
+ </ConfigContextProvider>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
new file mode 100644
index 000000000..414eee39d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -0,0 +1,175 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useState } from "preact/hooks";
+import { InstanceRoutes } from "./InstanceRoutes.js";
+import {
+ NotConnectedAppMenu,
+ NotYetReadyAppMenu,
+ NotificationCard,
+} from "./components/menu/index.js";
+import { useBackendContext } from "./context/backend.js";
+import { LoginToken } from "./declaration.js";
+import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
+import { LoginPage } from "./paths/login/index.js";
+import { Settings } from "./paths/settings/index.js";
+import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
+
+/**
+ * Check if admin against /management/instances
+ * @returns
+ */
+export function ApplicationReadyRoutes(): VNode {
+ const { i18n } = useTranslationContext();
+ const [unauthorized, setUnauthorized] = useState(false)
+ const {
+ url: backendURL,
+ updateToken,
+ alreadyTriedLogin,
+ } = useBackendContext();
+
+ function updateLoginStatus(token: LoginToken | undefined) {
+ updateToken(token)
+ setUnauthorized(false)
+ }
+
+ const result = useBackendInstancesTestForAdmin();
+
+ const clearTokenAndGoToRoot = () => {
+ route("/");
+ };
+ const [showSettings, setShowSettings] = useState(false)
+ const unauthorizedAdmin = !result.loading
+ && !result.ok
+ && result.type === ErrorType.CLIENT
+ && result.status === HttpStatusCode.Unauthorized;
+
+ if (!alreadyTriedLogin && !result.ok) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Welcome!" />
+ <LoginPage onConfirm={updateToken} />
+ </Fragment>
+ );
+ }
+
+ if (showSettings) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <Settings onClose={() => setShowSettings(false)} />
+ </Fragment>
+ }
+
+ if (result.loading) {
+ return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />;
+ }
+
+ let admin = result.ok || unauthorizedAdmin;
+ let instanceNameByBackendURL: string | undefined;
+
+ if (!admin) {
+ // * the testing against admin endpoint failed and it's not
+ // an authorization problem
+ // * merchant backend will return this SPA under the main
+ // endpoint or /instance/<id> endpoint
+ // => trying to infer the instance id
+ const path = new URL(backendURL).pathname;
+ const match = INSTANCE_ID_LOOKUP.exec(path);
+ if (!match || !match[1]) {
+ // this should be rare because
+ // query to /config is ok but the URL
+ // does not match our pattern
+ return (
+ <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Couldn't access the server.`,
+ description: i18n.str`Could not infer instance id from url ${backendURL}`,
+ type: "ERROR",
+ }}
+ />
+ {/* <ConnectionPage onConfirm={changeBackend} /> */}
+ </Fragment>
+ );
+ }
+
+ instanceNameByBackendURL = match[1];
+ }
+
+ if (unauthorized || unauthorizedAdmin) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Check your token is valid`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ }
+
+ const history = createHashHistory();
+ return (
+ <Router history={history}>
+ <Route
+ default
+ component={DefaultMainRoute}
+ admin={admin}
+ onUnauthorized={() => setUnauthorized(true)}
+ onLoginPass={() => {
+ setUnauthorized(false)
+ }}
+ instanceNameByBackendURL={instanceNameByBackendURL}
+ />
+ </Router>
+ );
+}
+
+function DefaultMainRoute({
+ instance,
+ admin,
+ onUnauthorized,
+ onLoginPass,
+ instanceNameByBackendURL,
+ url, //from preact-router
+}: any): VNode {
+ const [instanceName, setInstanceName] = useState(
+ instanceNameByBackendURL || instance || "default",
+ );
+
+ return (
+ <InstanceRoutes
+ admin={admin}
+ path={url}
+ onUnauthorized={onUnauthorized}
+ onLoginPass={onLoginPass}
+ id={instanceName}
+ setInstanceName={setInstanceName}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
new file mode 100644
index 000000000..14ccf773a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
@@ -0,0 +1,800 @@
+/*
+ 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)
+ * @author Nic Eigel
+ */
+
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, FunctionComponent, h, VNode } from "preact";
+import { Route, route, Router } from "preact-router";
+import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
+import { Loading } from "./components/exception/loading.js";
+import { Menu, NotificationCard } from "./components/menu/index.js";
+import { useBackendContext } from "./context/backend.js";
+import { InstanceContextProvider } from "./context/instance.js";
+import {
+ useBackendDefaultToken,
+ useBackendInstanceToken,
+ useSimpleLocalStorage,
+} from "./hooks/index.js";
+import { useInstanceKYCDetails } from "./hooks/instance.js";
+import InstanceCreatePage from "./paths/admin/create/index.js";
+import InstanceListPage from "./paths/admin/list/index.js";
+import TokenPage from "./paths/instance/token/index.js";
+import ListKYCPage from "./paths/instance/kyc/list/index.js";
+import OrderCreatePage from "./paths/instance/orders/create/index.js";
+import OrderDetailsPage from "./paths/instance/orders/details/index.js";
+import OrderListPage from "./paths/instance/orders/list/index.js";
+import DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js";
+import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js";
+import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/update/index.js";
+import ProductCreatePage from "./paths/instance/products/create/index.js";
+import ProductListPage from "./paths/instance/products/list/index.js";
+import ProductUpdatePage from "./paths/instance/products/update/index.js";
+import BankAccountCreatePage from "./paths/instance/accounts/create/index.js";
+import BankAccountListPage from "./paths/instance/accounts/list/index.js";
+import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js";
+import ReservesCreatePage from "./paths/instance/reserves/create/index.js";
+import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
+import ReservesListPage from "./paths/instance/reserves/list/index.js";
+import TemplateCreatePage from "./paths/instance/templates/create/index.js";
+import TemplateUsePage from "./paths/instance/templates/use/index.js";
+import TemplateQrPage from "./paths/instance/templates/qr/index.js";
+import TemplateListPage from "./paths/instance/templates/list/index.js";
+import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
+import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
+import WebhookListPage from "./paths/instance/webhooks/list/index.js";
+import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
+import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js";
+import ValidatorListPage from "./paths/instance/otp_devices/list/index.js";
+import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js";
+import TransferCreatePage from "./paths/instance/transfers/create/index.js";
+import TransferListPage from "./paths/instance/transfers/list/index.js";
+import InstanceUpdatePage, {
+ AdminUpdate as InstanceAdminUpdatePage,
+ Props as InstanceUpdatePageProps,
+} from "./paths/instance/update/index.js";
+import { LoginPage } from "./paths/login/index.js";
+import NotFoundPage from "./paths/notfound/index.js";
+import { Notification } from "./utils/types.js";
+import { LoginToken, MerchantBackend } from "./declaration.js";
+import { Settings } from "./paths/settings/index.js";
+import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
+
+export enum InstancePaths {
+ error = "/error",
+ settings = "/settings",
+ token = "/token",
+
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
+
+ deposit_confirmation_list = "/deposit-confirmation",
+ deposit_confirmation_update = "/deposit-confirmation/:pid/update",
+ deposit_confirmation_new = "/deposit-confirmation/new",
+
+ interface = "/interface",
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const noop = () => { };
+
+export enum AdminPaths {
+ list_instances = "/instances",
+ new_instance = "/instance/new",
+ update_instance = "/instance/:id/update",
+}
+
+export interface Props {
+ id: string;
+ admin?: boolean;
+ path: string;
+ onUnauthorized: () => void;
+ onLoginPass: () => void;
+ setInstanceName: (s: string) => void;
+}
+
+export function InstanceRoutes({
+ id,
+ admin,
+ path,
+ // onUnauthorized,
+ onLoginPass,
+ setInstanceName,
+}: Props): VNode {
+ const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
+ const [token, updateToken] = useBackendInstanceToken(id);
+ const { i18n } = useTranslationContext();
+
+ type GlobalNotifState = (Notification & { to: string }) | undefined;
+ const [globalNotification, setGlobalNotification] =
+ useState<GlobalNotifState>(undefined);
+
+ const changeToken = (token?: LoginToken) => {
+ if (admin) {
+ updateToken(token);
+ } else {
+ updateDefaultToken(token);
+ }
+ onLoginPass()
+ };
+ // const updateLoginStatus = (url: string, token?: string) => {
+ // changeToken(token);
+ // };
+
+ const value = useMemo(
+ () => ({ id, token, admin, changeToken }),
+ [id, token, admin],
+ );
+
+ function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
+ return function ServerErrorRedirectToImpl(
+ error: HttpError<MerchantBackend.ErrorDetail>,
+ ) {
+ if (error.type === ErrorType.TIMEOUT) {
+ setGlobalNotification({
+ message: i18n.str`The request to the backend take too long and was cancelled`,
+ description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
+ type: "ERROR",
+ to,
+ });
+ } else {
+ setGlobalNotification({
+ message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ details:
+ error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
+ ? error.payload.detail
+ : undefined,
+ type: "ERROR",
+ to,
+ });
+ }
+ return <Redirect to={to} />;
+ };
+ }
+
+ // const LoginPageAccessDeniend = onUnauthorized
+ const LoginPageAccessDenied = () => {
+ return <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Session expired or password changed.`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={changeToken} />
+ </Fragment>
+
+ }
+
+ function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
+ return function IfAdminCreateDefaultOrImpl(props?: T) {
+ if (admin && id === "default") {
+ return (
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`No 'default' instance configured yet.`,
+ description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
+ type: "INFO",
+ }}
+ />
+ <InstanceCreatePage
+ forceId="default"
+ onConfirm={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (props) {
+ return <Next {...props} />;
+ }
+ return <Next />;
+ };
+ }
+
+ const clearTokenAndGoToRoot = () => {
+ route("/");
+ // clear all tokens
+ updateToken(undefined)
+ updateDefaultToken(undefined)
+ };
+
+ return (
+ <InstanceContextProvider value={value}>
+ <Menu
+ instance={id}
+ admin={admin}
+ onShowSettings={() => {
+ route(InstancePaths.interface)
+ }}
+ path={path}
+ onLogout={clearTokenAndGoToRoot}
+ setInstanceName={setInstanceName}
+ isPasswordOk={defaultToken !== undefined}
+ />
+ <KycBanner />
+ <NotificationCard notification={globalNotification} />
+
+ <Router
+ onChange={(e) => {
+ const movingOutFromNotification =
+ globalNotification && e.url !== globalNotification.to;
+ if (movingOutFromNotification) {
+ setGlobalNotification(undefined);
+ }
+ }}
+ >
+ <Route path="/" component={Redirect} to={InstancePaths.order_list} />
+ {/**
+ * Admin pages
+ */}
+ {admin && (
+ <Route
+ path={AdminPaths.list_instances}
+ component={InstanceListPage}
+ onCreate={() => {
+ route(AdminPaths.new_instance);
+ }}
+ onUpdate={(id: string): void => {
+ route(`/instance/${id}/update`);
+ }}
+ setInstanceName={setInstanceName}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ )}
+ {admin && (
+ <Route
+ path={AdminPaths.new_instance}
+ component={InstanceCreatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ )}
+ {admin && (
+ <Route
+ path={AdminPaths.update_instance}
+ component={AdminInstanceUpdatePage}
+ onBack={() => route(AdminPaths.list_instances)}
+ onConfirm={() => {
+ route(AdminPaths.list_instances);
+ }}
+ onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)}
+ onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)}
+ onNotFound={NotFoundPage}
+ />
+ )}
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.settings}
+ component={InstanceUpdatePage}
+ onBack={() => {
+ route(`/`);
+ }}
+ onConfirm={() => {
+ route(`/`);
+ }}
+ onUpdateError={noop}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ {/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.token}
+ component={TokenPage}
+ onChange={() => {
+ route(`/`);
+ }}
+ onCancel={() => {
+ route(InstancePaths.order_list)
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ {/**
+ * Inventory pages
+ */}
+ <Route
+ path={InstancePaths.inventory_list}
+ component={ProductListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.inventory_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.inventory_update.replace(":pid", id));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.inventory_update}
+ component={ProductUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.inventory_new}
+ component={ProductCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.inventory_list);
+ }}
+ />
+ {/**
+ * Deposit confirmation pages
+ */}
+ <Route
+ path={InstancePaths.deposit_confirmation_list}
+ component={DepositConfirmationListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.deposit_confirmation_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.deposit_confirmation_update.replace(":pid", id));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.deposit_confirmation_update}
+ component={DepositConfirmationUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)}
+ onConfirm={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.deposit_confirmation_new}
+ component={DepositConfirmationCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.deposit_confirmation_list);
+ }}
+ />
+ {/**
+ * Bank pages
+ */}
+ <Route
+ path={InstancePaths.bank_list}
+ component={BankAccountListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.bank_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.bank_update.replace(":bid", id));
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.bank_update}
+ component={BankAccountUpdatePage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.bank_new}
+ component={BankAccountCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ {/**
+ * Order pages
+ */}
+ <Route
+ path={InstancePaths.order_list}
+ component={OrderListPage}
+ onCreate={() => {
+ route(InstancePaths.order_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.order_details}
+ component={OrderDetailsPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.order_new}
+ component={OrderCreatePage}
+ onConfirm={(orderId: string) => {
+ route(InstancePaths.order_details.replace(":oid", orderId));
+ }}
+ onBack={() => {
+ route(InstancePaths.order_list);
+ }}
+ />
+ {/**
+ * Transfer pages
+ */}
+ <Route
+ path={InstancePaths.transfers_list}
+ component={TransferListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.transfers_new);
+ }}
+ />
+ <Route
+ path={InstancePaths.transfers_new}
+ component={TransferCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.transfers_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.transfers_list);
+ }}
+ />
+ {/**
+ * Webhooks pages
+ */}
+ <Route
+ path={InstancePaths.webhooks_list}
+ component={WebhookListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.webhooks_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.webhooks_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_update}
+ component={WebhookUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.webhooks_new}
+ component={WebhookCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.webhooks_list);
+ }}
+ />
+ {/**
+ * Validator pages
+ */}
+ <Route
+ path={InstancePaths.otp_devices_list}
+ component={ValidatorListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.otp_devices_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.otp_devices_update.replace(":vid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_update}
+ component={ValidatorUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.otp_devices_new}
+ component={ValidatorCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.otp_devices_list);
+ }}
+ />
+ {/**
+ * Templates pages
+ */}
+ <Route
+ path={InstancePaths.templates_list}
+ component={TemplateListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onCreate={() => {
+ route(InstancePaths.templates_new);
+ }}
+ onNewOrder={(id: string) => {
+ route(InstancePaths.templates_use.replace(":tid", id));
+ }}
+ onQR={(id: string) => {
+ route(InstancePaths.templates_qr.replace(":tid", id));
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.templates_update.replace(":tid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_update}
+ component={TemplateUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_new}
+ component={TemplateCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.templates_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_use}
+ component={TemplateUsePage}
+ onOrderCreated={(id: string) => {
+ route(InstancePaths.order_details.replace(":oid", id));
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.templates_qr}
+ component={TemplateQrPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.templates_list);
+ }}
+ />
+
+ {/**
+ * reserves pages
+ */}
+ <Route
+ path={InstancePaths.reserves_list}
+ component={ReservesListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
+ onSelect={(id: string) => {
+ route(InstancePaths.reserves_details.replace(":rid", id));
+ }}
+ onCreate={() => {
+ route(InstancePaths.reserves_new);
+ }}
+ />
+ <Route
+ path={InstancePaths.reserves_details}
+ component={ReservesDetailsPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.reserves_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.reserves_new}
+ component={ReservesCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.reserves_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.reserves_list);
+ }}
+ />
+ <Route path={InstancePaths.kyc} component={ListKYCPage} />
+ <Route path={InstancePaths.interface} component={Settings} />
+ {/**
+ * Example pages
+ */}
+ <Route path="/loading" component={Loading} />
+ <Route default component={NotFoundPage} />
+ </Router>
+ </InstanceContextProvider>
+ );
+}
+
+export function Redirect({ to }: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+}
+
+function AdminInstanceUpdatePage({
+ id,
+ ...rest
+}: { id: string } & InstanceUpdatePageProps): VNode {
+ const [token, changeToken] = useBackendInstanceToken(id);
+ const updateLoginStatus = (token?: LoginToken): void => {
+ changeToken(token);
+ };
+ const value = useMemo(
+ () => ({ id, token, admin: true, changeToken }),
+ [id, token],
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InstanceContextProvider value={value}>
+ <InstanceAdminUpdatePage
+ {...rest}
+ instanceId={id}
+ onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => {
+ const notif =
+ error.type === ErrorType.TIMEOUT
+ ? {
+ message: i18n.str`The request to the backend take too long and was cancelled`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ type: "ERROR" as const,
+ }
+ : {
+ message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ details:
+ error.type === ErrorType.CLIENT ||
+ error.type === ErrorType.SERVER
+ ? error.payload.detail
+ : undefined,
+ type: "ERROR" as const,
+ };
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ );
+ }}
+ onUnauthorized={() => {
+ return (
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`The access token provided is invalid`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={updateLoginStatus} />
+ </Fragment>
+ );
+ }}
+ />
+ </InstanceContextProvider>
+ );
+}
+
+function KycBanner(): VNode {
+ const kycStatus = useInstanceKYCDetails();
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ const today = format(new Date(), dateFormatForSettings(settings));
+ const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide");
+ const hasBeenHidden = today === lastHide;
+ const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
+ if (hasBeenHidden || !needsToBeShown) return <Fragment />;
+ return (
+ <NotificationCard
+ notification={{
+ type: "WARN",
+ message: "KYC verification needed",
+ description: (
+ <div>
+ <p>
+ Some transfer are on hold until a KYC process is completed. Go to
+ the KYC section in the left panel for more information
+ </p>
+ <div class="buttons is-right">
+ <button class="button" onClick={() => setLastHide(today)}>
+ <i18n.Translate>Hide for today</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ ),
+ }}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/assets/empty.png b/packages/auditor-backoffice-ui/src/assets/empty.png
new file mode 100644
index 000000000..5120d3138
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/empty.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png
new file mode 100644
index 000000000..93ebe2e2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-192x192.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png
new file mode 100644
index 000000000..52d1623ea
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/android-chrome-512x512.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png
new file mode 100644
index 000000000..254e4bb4d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/apple-touch-icon.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png
new file mode 100644
index 000000000..e81177dcb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-16x16.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png
new file mode 100644
index 000000000..40e9b5b47
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/favicon-32x32.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg
new file mode 100644
index 000000000..22d58da65
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/languageicon.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png
new file mode 100644
index 000000000..9cfb889be
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/icons/mstile-150x150.png
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/assets/logo-2021.svg b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/assets/logo.jpeg b/packages/auditor-backoffice-ui/src/assets/logo.jpeg
new file mode 100644
index 000000000..489832f7c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/assets/logo.jpeg
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
new file mode 100644
index 000000000..b1fc33877
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
@@ -0,0 +1,55 @@
+/*
+ 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)
+ */
+
+import { ComponentChildren, h } from "preact";
+import { LoadingModal } from "../modal/index.js";
+import { useAsync } from "../../hooks/async.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+type Props = {
+ children: ComponentChildren;
+ disabled: boolean;
+ onClick?: () => Promise<void>;
+ [rest: string]: any;
+};
+
+export function AsyncButton({ onClick, disabled, children, ...rest }: Props) {
+ const { isSlow, isLoading, request, cancel } = useAsync(onClick);
+ const { i18n } = useTranslationContext();
+ if (isSlow) {
+ return <LoadingModal onCancel={cancel} />;
+ }
+ if (isLoading) {
+ return (
+ <button class="button">
+ <i18n.Translate>Loading...</i18n.Translate>
+ </button>
+ );
+ }
+
+ return (
+ <span {...rest}>
+ <button class="button is-success" onClick={request} disabled={disabled}>
+ {children}
+ </button>
+ </span>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx
new file mode 100644
index 000000000..c9340ea76
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/exception/QR.tsx
@@ -0,0 +1,49 @@
+/*
+ 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/>
+ */
+
+import { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ const qr = qrcode(0, "L");
+ qr.addData(text);
+ qr.make();
+ if (divRef.current) {
+ 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/auditor-backoffice-ui/src/components/exception/loading.tsx b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
new file mode 100644
index 000000000..a043b81eb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
@@ -0,0 +1,48 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+
+export function Loading(): VNode {
+ return (
+ <div
+ class="columns is-centered is-vcentered"
+ style={{
+ height: "calc(100% - 3rem)",
+ position: "absolute",
+ width: "100%",
+ }}
+ >
+ <Spinner />
+ </div>
+ );
+}
+
+export function Spinner(): VNode {
+ return (
+ <div class="lds-ring">
+ <div />
+ <div />
+ <div />
+ <div />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx
new file mode 100644
index 000000000..0d53c4d08
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx
@@ -0,0 +1,109 @@
+/*
+ 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)
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useMemo } from "preact/hooks";
+
+type Updater<S> = (value: (prevState: S) => S) => void;
+
+export interface Props<T> {
+ object?: Partial<T>;
+ errors?: FormErrors<T>;
+ name?: string;
+ valueHandler: Updater<Partial<T>> | null;
+ children: ComponentChildren;
+}
+
+const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s;
+
+export function FormProvider<T>({
+ object = {},
+ errors = {},
+ name = "",
+ valueHandler,
+ children,
+}: Props<T>): VNode {
+ const initialObject = useMemo(() => object, []);
+ const value = useMemo<FormType<T>>(
+ () => ({
+ errors,
+ object,
+ initialObject,
+ valueHandler: valueHandler ? valueHandler : noUpdater,
+ name,
+ toStr: {},
+ fromStr: {},
+ }),
+ [errors, object, valueHandler],
+ );
+
+ return (
+ <FormContext.Provider value={value}>
+ <form
+ class="field"
+ onSubmit={(e) => {
+ e.preventDefault();
+ // if (valueHandler) valueHandler(object);
+ }}
+ >
+ {children}
+ </form>
+ </FormContext.Provider>
+ );
+}
+
+export interface FormType<T> {
+ object: Partial<T>;
+ initialObject: Partial<T>;
+ errors: FormErrors<T>;
+ toStr: FormtoStr<T>;
+ name: string;
+ fromStr: FormfromStr<T>;
+ valueHandler: Updater<Partial<T>>;
+}
+
+const FormContext = createContext<FormType<unknown>>(null!);
+
+/**
+ * FIXME:
+ * USE MEMORY EVENTS INSTEAD OF CONTEXT
+ * @deprecated
+ */
+
+export function useFormContext<T>() {
+ return useContext<FormType<T>>(FormContext);
+}
+
+export type FormErrors<T> = {
+ [P in keyof T]?: string | FormErrors<T[P]>;
+};
+
+export type FormtoStr<T> = {
+ [P in keyof T]?: (f?: T[P]) => string;
+};
+
+export type FormfromStr<T> = {
+ [P in keyof T]?: (f: string) => T[P];
+};
+
+export type FormUpdater<T> = {
+ [P in keyof T]?: (f: keyof T) => (v: T[P]) => void;
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/Input.tsx b/packages/auditor-backoffice-ui/src/components/form/Input.tsx
new file mode 100644
index 000000000..c1ddcb064
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/Input.tsx
@@ -0,0 +1,116 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: "text" | "number" | "multiline" | "password";
+ expand?: boolean;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any;
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+const TextInput = ({ inputType, error, ...rest }: any) =>
+ inputType === "multiline" ? (
+ <textarea
+ {...rest}
+ class={error ? "textarea is-danger" : "textarea"}
+ rows="3"
+ />
+ ) : (
+ <input
+ {...rest}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ />
+ );
+
+export function Input<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ expand,
+ help,
+ children,
+ inputType,
+ inputExtra,
+ side,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ <TextInput
+ error={error}
+ {...inputExtra}
+ inputType={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void =>
+ onChange(fromStr(e.currentTarget.value))
+ }
+ />
+ {help}
+ {children}
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx
new file mode 100644
index 000000000..4ed4c4b28
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx
@@ -0,0 +1,139 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+ addonBefore?: string;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputArray<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ addonBefore,
+ isValid = () => true,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error: formError, value, onChange, required } = useField<T>(name);
+ const [localError, setLocalError] = useState<string | null>(null);
+
+ const error = localError || formError;
+
+ const array: any[] = (value ? value! : []) as any;
+ const [currentValue, setCurrentValue] = useState("");
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" 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">
+ {addonBefore && (
+ <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>
+ )}
+ <p class="control is-expanded has-icons-right">
+ <input
+ class={error ? "input is-danger" : "input"}
+ type="text"
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={currentValue}
+ onChange={(e): void => setCurrentValue(e.currentTarget.value)}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ </p>
+ <p class="control">
+ <button
+ class="button is-info has-tooltip-left"
+ disabled={!currentValue}
+ onClick={(): void => {
+ const v = fromStr(currentValue);
+ if (!isValid(v)) {
+ setLocalError(
+ i18n.str`The value ${v} is invalid for a payment url`,
+ );
+ return;
+ }
+ setLocalError(null);
+ onChange([v, ...array] as any);
+ setCurrentValue("");
+ }}
+ data-tooltip={i18n.str`add element to the list`}
+ >
+ <i18n.Translate>add</i18n.Translate>
+ </button>
+ </p>
+ </div>
+ {help}
+ {error && <p class="help is-danger"> {error} </p>}
+ {array.map((v, i) => (
+ <div key={i} class="tags has-addons mt-3 mb-0">
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ {v}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(array.filter((f) => f !== v) as any);
+ setCurrentValue(toStr(v));
+ }}
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx
new file mode 100644
index 000000000..f79e16c07
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx
@@ -0,0 +1,91 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputBoolean<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : ""}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <span class="check" />
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
new file mode 100644
index 000000000..b02354d7c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -0,0 +1,67 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useConfigContext } from "../../context/config.js";
+import { Amount } from "../../declaration.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { InputProps } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+ side?: ComponentChildren;
+}
+
+export function InputCurrency<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ addonAfter,
+ children,
+ side,
+}: Props<keyof T>): VNode {
+ const config = useConfigContext();
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ addonBefore={config.currency}
+ side={side}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ addonAfter={addonAfter}
+ inputType="number"
+ expand={expand}
+ toStr={(v?: Amount) => v?.split(":")[1] || ""}
+ fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)}
+ inputExtra={{ min: 0 }}
+ >
+ {children}
+ </InputWithAddon>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx
new file mode 100644
index 000000000..a398629dc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx
@@ -0,0 +1,164 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../picker/DatePicker.js";
+import { InputProps, useField } from "./useField.js";
+import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ //FIXME: create separated components InputDate and InputTimestamp
+ withTimestampSupport?: boolean;
+ side?: ComponentChildren;
+}
+
+export function InputDate<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ withTimestampSupport,
+ side,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+
+ const { error, required, value, onChange } = useField<T>(name);
+
+ let strValue = "";
+ if (!value) {
+ strValue = withTimestampSupport ? "unknown" : "";
+ } else if (value instanceof Date) {
+ strValue = format(value, dateFormatForSettings(settings));
+ } else if (value.t_s) {
+ strValue =
+ value.t_s === "never"
+ ? withTimestampSupport
+ ? "never"
+ : ""
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
+ }
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" 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 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);
+ }}
+ >
+ <a class="button is-static">
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+
+ {!readonly && (
+ <span
+ data-tooltip={
+ withTimestampSupport
+ ? i18n.str`change value to unknown date`
+ : i18n.str`change value to empty`
+ }
+ >
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange(undefined as any)}
+ >
+ <i18n.Translate>clear</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {withTimestampSupport && (
+ <span data-tooltip={i18n.str`change value to never`}>
+ <button
+ class="button is-info"
+ onClick={() => onChange({ t_s: "never" } as any)}
+ >
+ <i18n.Translate>never</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {side}
+ </div>
+ <DatePicker
+ opened={opened}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ if (withTimestampSupport) {
+ onChange({ t_s: d.getTime() / 1000 } as any);
+ } else {
+ onChange(d as any);
+ }
+ }}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx
new file mode 100644
index 000000000..7aa2703a4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx
@@ -0,0 +1,186 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { formatDuration, intervalToDuration } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { SimpleModal } from "../modal/index.js";
+import { DurationPicker } from "../picker/DurationPicker.js";
+import { InputProps, useField } from "./useField.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ readonly?: boolean;
+ withForever?: boolean;
+ side?: ComponentChildren;
+ withoutClear?: boolean;
+}
+
+export function InputDuration<T>({
+ name,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ readonly,
+ withForever,
+ withoutClear,
+ side,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const { i18n } = useTranslationContext();
+
+ const { error, required, value: anyValue, onChange } = useField<T>(name);
+ let strValue = "";
+ const value: Duration = anyValue
+ if (!value) {
+ strValue = "";
+ } else if (value.d_ms === "forever") {
+ strValue = i18n.str`forever`;
+ } else {
+ strValue = formatDuration(
+ intervalToDuration({ start: 0, end: value.d_ms }),
+ {
+ locale: {
+ formatDistance: (name, value) => {
+ switch (name) {
+ case "xMonths":
+ return i18n.str`${value}M`;
+ case "xYears":
+ return i18n.str`${value}Y`;
+ case "xDays":
+ return i18n.str`${value}d`;
+ case "xHours":
+ return i18n.str`${value}h`;
+ case "xMinutes":
+ return i18n.str`${value}min`;
+ case "xSeconds":
+ return i18n.str`${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 is-flex-grow-3">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+
+ <div class="is-flex-grow-3">
+ <div class="field-body ">
+ <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>
+ )}
+ </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>
+ {withForever && (
+ <span data-tooltip={i18n.str`change value to never`}>
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange({ d_ms: "forever" } as any)}
+ >
+ <i18n.Translate>forever</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {!readonly && !withoutClear && (
+ <span data-tooltip={i18n.str`change value to empty`}>
+ <button
+ class="button is-info "
+ onClick={() => onChange(undefined as any)}
+ >
+ <i18n.Translate>clear</i18n.Translate>
+ </button>
+ </span>
+ )}
+ {side}
+ </div>
+ <span>
+ {help}
+ </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/auditor-backoffice-ui/src/components/form/InputGroup.tsx b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx
new file mode 100644
index 000000000..b5e0bd52b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx
@@ -0,0 +1,86 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useGroupField } from "./useGroupField.js";
+
+export interface Props<T> {
+ name: T;
+ children: ComponentChildren;
+ label: ComponentChildren;
+ tooltip?: ComponentChildren;
+ alternative?: ComponentChildren;
+ fixed?: boolean;
+ initialActive?: boolean;
+}
+
+export function InputGroup<T>({
+ name,
+ label,
+ children,
+ tooltip,
+ alternative,
+ fixed,
+ initialActive,
+}: Props<keyof T>): VNode {
+ const [active, setActive] = useState(initialActive || fixed);
+ const group = useGroupField<T>(name);
+
+ return (
+ <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" 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>
+ {!fixed && (
+ <button
+ class="card-header-icon"
+ aria-label="more options"
+ onClick={(): void => setActive(!active)}
+ >
+ <span class="icon">
+ {active ? (
+ <i class="mdi mdi-arrow-up" />
+ ) : (
+ <i class="mdi mdi-arrow-down" />
+ )}
+ </span>
+ </button>
+ )}
+ </header>
+ {active ? (
+ <div class="card-content">{children}</div>
+ ) : alternative ? (
+ <div class="card-content">{alternative}</div>
+ ) : undefined}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx
new file mode 100644
index 000000000..b024e2c6b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx
@@ -0,0 +1,122 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputImage<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ children,
+ expand,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const image = useRef<HTMLInputElement>(null);
+ const { i18n } = useTranslationContext();
+ const [sizeError, setSizeError] = useState(false);
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ {value && (
+ <img
+ src={value}
+ style={{ width: 200, height: 200 }}
+ onClick={() => image.current?.click()}
+ />
+ )}
+ <input
+ ref={image}
+ style={{ display: "none" }}
+ type="file"
+ name={String(name)}
+ placeholder={placeholder}
+ readonly={readonly}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return onChange(undefined!);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ {help}
+ {children}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ {sizeError && (
+ <p class="help is-danger">
+ <i18n.Translate>Image should be smaller than 1 MB</i18n.Translate>
+ </p>
+ )}
+ {!value && (
+ <button class="button" onClick={() => image.current?.click()}>
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ )}
+ {value && (
+ <button class="button" onClick={() => onChange(undefined!)}>
+ <i18n.Translate>Remove</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx
new file mode 100644
index 000000000..a2fc8113e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx
@@ -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)
+ */
+import { Fragment, h } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Input } from "./Input.js";
+
+export function InputLocation({ name }: { name: string }) {
+ const { i18n } = useTranslationContext();
+ return (
+ <>
+ <Input name={`${name}.country`} label={i18n.str`Country`} />
+ <Input
+ name={`${name}.address_lines`}
+ inputType="multiline"
+ label={i18n.str`Address`}
+ toStr={(v: string[] | undefined) => (!v ? "" : v.join("\n"))}
+ fromStr={(v: string) => v.split("\n")}
+ />
+ <Input
+ name={`${name}.building_number`}
+ label={i18n.str`Building number`}
+ />
+ <Input name={`${name}.building_name`} label={i18n.str`Building name`} />
+ <Input name={`${name}.street`} label={i18n.str`Street`} />
+ <Input name={`${name}.post_code`} label={i18n.str`Post code`} />
+ <Input name={`${name}.town_location`} label={i18n.str`Town location`} />
+ <Input name={`${name}.town`} label={i18n.str`Town`} />
+ <Input name={`${name}.district`} label={i18n.str`District`} />
+ <Input
+ name={`${name}.country_subdivision`}
+ label={i18n.str`Country subdivision`}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
new file mode 100644
index 000000000..3b5df1474
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
@@ -0,0 +1,60 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h } from "preact";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { InputProps } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputNumber<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ expand,
+ children,
+ side,
+}: Props<keyof T>) {
+ return (
+ <InputWithAddon<T>
+ name={name}
+ readonly={readonly}
+ fromStr={(v) => (!v ? undefined : parseInt(v, 10))}
+ toStr={(v) => `${v}`}
+ inputType="number"
+ expand={expand}
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ inputExtra={{ min: 0 }}
+ children={children}
+ side={side}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx
new file mode 100644
index 000000000..6e88e8f2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx
@@ -0,0 +1,52 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { InputArray } from "./InputArray.js";
+import { PAYTO_REGEX } from "../../utils/constants.js";
+import { InputProps } from "./useField.js";
+
+export type Props<T> = InputProps<T>;
+
+const PAYTO_START_REGEX = /^payto:\/\//;
+
+export function InputPayto<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+}: Props<keyof T>): VNode {
+ return (
+ <InputArray<T>
+ name={name}
+ readonly={readonly}
+ addonBefore="payto://"
+ label={label}
+ placeholder={placeholder}
+ help={help}
+ tooltip={tooltip}
+ isValid={(v) => v && PAYTO_REGEX.test(v)}
+ toStr={(v?: string) => (!v ? "" : v.replace(PAYTO_START_REGEX, ""))}
+ fromStr={(v: string) => `payto://${v}`}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
new file mode 100644
index 000000000..282e52278
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
@@ -0,0 +1,47 @@
+/*
+ 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)
+ */
+
+import { h } from "preact";
+import * as tests from "@gnu-taler/web-util/testing";
+import { InputPaytoForm } from "./InputPaytoForm.js";
+import { FormProvider } from "./FormProvider.js";
+import { useState } from "preact/hooks";
+
+export default {
+ title: "Components/Form/PayTo",
+ component: InputPaytoForm,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(() => {
+ const initial = {
+ accounts: [],
+ };
+ const [form, updateForm] = useState<Partial<typeof initial>>(initial);
+ return (
+ <FormProvider valueHandler={updateForm} object={form}>
+ <InputPaytoForm name="accounts" label="Accounts:" />
+ </FormProvider>
+ );
+}, {});
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx
new file mode 100644
index 000000000..32545c89a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -0,0 +1,397 @@
+/*
+ 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)
+ */
+import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { COUNTRY_TABLE } from "../../utils/constants.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { Input } from "./Input.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputSelector } from "./InputSelector.js";
+import { InputProps, useField } from "./useField.js";
+import { useEffect, useState } from "preact/hooks";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+// type Entity = PaytoUriGeneric
+// https://datatracker.ietf.org/doc/html/rfc8905
+type Entity = {
+ // iban, bitcoin, x-taler-bank. it defined the format
+ target: string;
+ // path1 if the first field to be used
+ path1?: string;
+ // path2 if the second field to be used, optional
+ path2?: string;
+ // params of the payto uri
+ params: {
+ "receiver-name"?: string;
+ sender?: string;
+ message?: string;
+ amount?: string;
+ instruction?: string;
+ [name: string]: string | undefined;
+ };
+};
+
+function isEthereumAddress(address: string) {
+ if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
+ return false;
+ } else if (
+ /^(0x|0X)?[0-9a-f]{40}$/.test(address) ||
+ /^(0x|0X)?[0-9A-F]{40}$/.test(address)
+ ) {
+ return true;
+ }
+ return checkAddressChecksum(address);
+}
+
+function checkAddressChecksum(address: string) {
+ //TODO implement ethereum checksum
+ return true;
+}
+
+function validateBitcoin(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ try {
+ const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid bitcoin address.`;
+}
+
+function validateEthereum(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ try {
+ const valid = isEthereumAddress(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid Ethereum address.`;
+}
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
+ *
+ */
+function validateIBAN(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n.str`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n.str`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+ // check supported country
+ const code = IBAN.substr(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n.str`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substr(4) + iban.substr(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substr(0, 5);
+ const rest = str.substr(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+ }
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank']
+const targets = [
+ "Choose one...",
+ "iban",
+ "x-taler-bank",
+ "bitcoin",
+ "ethereum",
+];
+const noTargetValue = targets[0];
+const defaultTarget: Entity = {
+ target: noTargetValue,
+ params: {},
+};
+
+export function InputPaytoForm<T>({
+ name,
+ readonly,
+ label,
+ tooltip,
+}: Props<keyof T>): VNode {
+ const { value: initialValueStr, onChange } = useField<T>(name);
+
+ const initialPayto = parsePaytoUri(initialValueStr ?? "")
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
+ const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
+ const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
+ const initial: Entity = initialPayto === undefined ? defaultTarget : {
+ target: initialPayto.targetType,
+ params: initialPayto.params,
+ path1: initialPath1,
+ path2: initialPath2,
+ }
+ const [value, setValue] = useState<Partial<Entity>>(initial)
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ target:
+ value.target === noTargetValue
+ ? i18n.str`required`
+ : undefined,
+ path1: !value.path1
+ ? i18n.str`required`
+ : value.target === "iban"
+ ? validateIBAN(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum(value.path1, i18n)
+ : undefined,
+ path2:
+ value.target === "x-taler-bank"
+ ? !value.path2
+ ? i18n.str`required`
+ : undefined
+ : undefined,
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
+ ? i18n.str`required`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
+ params: value.params ?? {} as any,
+ isKnown: false,
+ })
+ useEffect(() => {
+ onChange(str as any)
+ }, [str])
+
+ // const submit = useCallback((): void => {
+ // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
+ // payto_uri: paytoURL,
+ // };
+ // if (value.auth) {
+ // if (value.auth.url) {
+ // newValue.credit_facade_url = value.auth.url;
+ // }
+ // if (value.auth.type === "none") {
+ // newValue.credit_facade_credentials = {
+ // type: "none",
+ // };
+ // }
+ // if (value.auth.type === "basic") {
+ // newValue.credit_facade_credentials = {
+ // type: "basic",
+ // username: value.auth.username ?? "",
+ // password: value.auth.password ?? "",
+ // };
+ // }
+ // }
+ // onChange(newValue as any);
+ // // }
+ // // valueHandler(defaultTarget);
+ // }, [value]);
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup name="payto" label={label} fixed tooltip={tooltip}>
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={setValue}
+ >
+ <InputSelector<Entity>
+ name="target"
+ label={i18n.str`Account type`}
+ tooltip={i18n.str`Method to use for wire transfer`}
+ values={targets}
+ readonly={readonly}
+ toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
+ />
+
+ {value.target === "ach" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`Routing`}
+ readonly={readonly}
+ tooltip={i18n.str`Routing number.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n.str`Account`}
+ readonly={readonly}
+ tooltip={i18n.str`Account number.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bic" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`Code`}
+ readonly={readonly}
+ tooltip={i18n.str`Business Identifier Code.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "iban" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n.str`IBAN`}
+ tooltip={i18n.str`International Bank Account Number.`}
+ readonly={readonly}
+ placeholder="DE1231231231"
+ inputExtra={{ style: { textTransform: "uppercase" } }}
+ />
+ </Fragment>
+ )}
+ {value.target === "upi" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Unified Payment Interface.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bitcoin" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Bitcoin protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ethereum" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Ethereum protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ilp" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Interledger protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "void" && <Fragment />}
+ {value.target === "x-taler-bank" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ readonly={readonly}
+ label={i18n.str`Host`}
+ tooltip={i18n.str`Bank host.`}
+ />
+ <Input<Entity>
+ name="path2"
+ readonly={readonly}
+ label={i18n.str`Account`}
+ tooltip={i18n.str`Bank account.`}
+ />
+ </Fragment>
+ )}
+
+ {/**
+ * Show additional fields apart from the payto
+ */}
+ {value.target !== noTargetValue && (
+ <Fragment>
+ <Input
+ name="params.receiver-name"
+ readonly={readonly}
+ label={i18n.str`Owner's name`}
+ tooltip={i18n.str`Legal name of the person holding the account.`}
+ />
+ </Fragment>
+ )}
+
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx
new file mode 100644
index 000000000..be5800d14
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -0,0 +1,204 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
+
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
+}
+
+interface Search {
+ name: string;
+}
+
+export function InputSearchOnList<T extends Entity>({
+ selected,
+ onChange,
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
+ name: "",
+ });
+
+ const errors: FormErrors<Search> = {
+ name: undefined,
+ };
+ const { i18n } = useTranslationContext();
+
+ if (selected) {
+ return (
+ <article class="media">
+ {withImage &&
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p class="media-meta">
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
+ </p>
+ <p>
+ <i18n.Translate>Description</i18n.Translate>:{" "}
+ {selected.description}
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ onClick={() => onChange(undefined)}
+ >
+ clear
+ </button>
+ </div>
+ </div>
+ </div>
+ </article>
+ );
+ }
+
+ return (
+ <FormProvider<Search>
+ errors={errors}
+ object={nameForm}
+ valueHandler={setNameForm}
+ >
+ <InputWithAddon<Search>
+ name="name"
+ label={label}
+ tooltip={i18n.str`enter description or id`}
+ addonAfter={
+ <span class="icon">
+ <i class="mdi mdi-magnify" />
+ </span>
+ }
+ >
+ <div>
+ <DropdownList
+ name={nameForm.name}
+ list={list}
+ onSelect={(p) => {
+ setNameForm({ name: "" });
+ onChange(p);
+ }}
+ withImage={!!withImage}
+ />
+ </div>
+ </InputWithAddon>
+ </FormProvider>
+ );
+}
+
+interface DropdownListProps<T extends Entity> {
+ name?: string;
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
+}
+
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
+ const { i18n } = useTranslationContext();
+ if (!name) {
+ /* FIXME
+ this BR is added to occupy the space that will be added when the
+ dropdown appears
+ */
+ return (
+ <div>
+ <br />
+ </div>
+ );
+ }
+ const filtered = list.filter(
+ (p) => p.id.includes(name) || p.description.includes(name),
+ );
+
+ return (
+ <div class="dropdown is-active">
+ <div
+ class="dropdown-menu"
+ id="dropdown-menu"
+ role="menu"
+ style={{ minWidth: "20rem" }}
+ >
+ <div class="dropdown-content">
+ {!filtered.length ? (
+ <div class="dropdown-item">
+ <i18n.Translate>
+ no match found with that description or id
+ </i18n.Translate>
+ </div>
+ ) : (
+ filtered.map((p) => (
+ <div
+ key={p.id}
+ class="dropdown-item"
+ onClick={() => onSelect(p)}
+ style={{ cursor: "pointer" }}
+ >
+ <article class="media">
+ {withImage &&
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}>
+ <img
+ src={p.image ? p.image : emptyImage}
+ style={{ width: 64, height: 64 }}
+ />
+ </div>
+ </div>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
+ <br />
+ {p.description}
+ </p>
+ </div>
+ </div>
+ </article>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx
new file mode 100644
index 000000000..12ce6c6aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "./FormProvider.js";
+import { InputSecured } from "./InputSecured.js";
+
+export default {
+ title: "Components/Form/InputSecured",
+ component: InputSecured,
+};
+
+type T = { auth_token: string | null };
+
+export const InitialValueEmpty = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: "" });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
+
+export const InitialValueToken = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: "token" });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
+
+export const InitialValueNull = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: null });
+ return (
+ <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+ );
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx
new file mode 100644
index 000000000..9d1a3ab8e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx
@@ -0,0 +1,186 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { InputProps, useField } from "./useField.js";
+
+export type Props<T> = InputProps<T>;
+
+const TokenStatus = ({ prev, post }: any) => {
+ const { i18n } = useTranslationContext();
+ if (
+ (prev === undefined || prev === null) &&
+ (post === undefined || post === null)
+ )
+ return null;
+ return prev === post ? null : post === null ? (
+ <span class="tag is-danger is-align-self-center ml-2">
+ <i18n.Translate>Deleting</i18n.Translate>
+ </span>
+ ) : (
+ <span class="tag is-warning is-align-self-center ml-2">
+ <i18n.Translate>Changing</i18n.Translate>
+ </span>
+ );
+};
+
+export function InputSecured<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+}: Props<keyof T>): VNode {
+ const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name);
+
+ const [active, setActive] = useState(false);
+ const [newValue, setNuewValue] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ {!active ? (
+ <Fragment>
+ <div class="field has-addons">
+ <button
+ class="button"
+ onClick={(): void => {
+ setActive(!active);
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <i18n.Translate>Manage access token</i18n.Translate>
+ </span>
+ </button>
+ <TokenStatus prev={initial} post={value} />
+ </div>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <div class="field has-addons">
+ <div class="control">
+ <a class="button is-static">secret-token:</a>
+ </div>
+ <div class="control is-expanded">
+ <input
+ class="input"
+ type="text"
+ placeholder={placeholder}
+ readonly={readonly || !active}
+ disabled={readonly || !active}
+ name={String(name)}
+ value={newValue}
+ onInput={(e): void => {
+ setNuewValue(e.currentTarget.value);
+ }}
+ />
+ {help}
+ </div>
+ <div class="control">
+ <button
+ class="button is-info"
+ disabled={fromStr(newValue) === value}
+ onClick={(): void => {
+ onChange(fromStr(newValue));
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-outline" />
+ </div>
+ <span>
+ <i18n.Translate>Update</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ </div>
+ </Fragment>
+ )}
+ {error ? <p class="help is-danger">{error}</p> : null}
+ </div>
+ </div>
+ {active && (
+ <div class="field is-horizontal">
+ <div class="field-body is-flex-grow-3">
+ <div class="level" style={{ width: "100%" }}>
+ <div class="level-right is-flex-grow-1">
+ <div class="level-item">
+ <button
+ class="button is-danger"
+ disabled={null === value || undefined === value}
+ onClick={(): void => {
+ onChange(null!);
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-open-variant" />
+ </div>
+ <span>
+ <i18n.Translate>Remove</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ <div class="level-item">
+ <button
+ class="button "
+ onClick={(): void => {
+ onChange(initial!);
+ setActive(!active);
+ setNuewValue("");
+ }}
+ >
+ <div class="icon is-left">
+ <i class="mdi mdi-lock-open-variant" />
+ </div>
+ <span>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
new file mode 100644
index 000000000..a8dad5d89
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
@@ -0,0 +1,94 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ values: any[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputSelector<T>({
+ name,
+ readonly,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ values,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-icons-right">
+ <p class={expand ? "control is-expanded select" : "control select "}>
+ <select
+ class={error ? "select is-danger" : "select"}
+ name={String(name)}
+ disabled={readonly}
+ readonly={readonly}
+ onChange={(e) => {
+ onChange(fromStr(e.currentTarget.value));
+ }}
+ >
+ {placeholder && <option>{placeholder}</option>}
+ {values.map((v, i) => {
+ return (
+ <option key={i} value={v} selected={value === v}>
+ {toStr(v)}
+ </option>
+ );
+ })}
+ </select>
+
+ {help}
+ </p>
+ {required && (
+ <span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx
new file mode 100644
index 000000000..668c65ea7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx
@@ -0,0 +1,162 @@
+/*
+ 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)
+ */
+
+import { addDays } from "date-fns";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "./FormProvider.js";
+import { InputStock, Stock } from "./InputStock.js";
+
+export default {
+ title: "Components/Form/InputStock",
+ component: InputStock,
+};
+
+type T = { stock?: Stock };
+
+export const CreateStockEmpty = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockUnknownRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockNoRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: "never" },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockWithRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 15,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithManagedStock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 100,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithInfiniteStock = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx
new file mode 100644
index 000000000..1d18685c5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx
@@ -0,0 +1,224 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h } from "preact";
+import { useLayoutEffect, useState } from "preact/hooks";
+import { MerchantBackend, Timestamp } from "../../declaration.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { InputDate } from "./InputDate.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputLocation } from "./InputLocation.js";
+import { InputNumber } from "./InputNumber.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ alreadyExist?: boolean;
+}
+
+type Entity = Stock;
+
+export interface Stock {
+ current: number;
+ lost: number;
+ sold: number;
+ address?: MerchantBackend.Location;
+ nextRestock?: Timestamp;
+}
+
+interface StockDelta {
+ incoming: number;
+ lost: number;
+}
+
+export function InputStock<T>({
+ name,
+ tooltip,
+ label,
+ alreadyExist,
+}: Props<keyof T>) {
+ const { error, value, onChange } = useField<T>(name);
+
+ const [errors, setErrors] = useState<FormErrors<Entity>>({});
+
+ const [formValue, valueHandler] = useState<Partial<Entity>>(value);
+ const [addedStock, setAddedStock] = useState<StockDelta>({
+ incoming: 0,
+ lost: 0,
+ });
+ const { i18n } = useTranslationContext();
+
+ useLayoutEffect(() => {
+ if (!formValue) {
+ onChange(undefined as any);
+ } else {
+ onChange({
+ ...formValue,
+ current: (formValue?.current || 0) + addedStock.incoming,
+ lost: (formValue?.lost || 0) + addedStock.lost,
+ } as any);
+ }
+ }, [formValue, addedStock]);
+
+ if (!formValue) {
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-addons">
+ {!alreadyExist ? (
+ <button
+ class="button"
+ data-tooltip={i18n.str`click here to configure the stock of the product, leave it as is and the backend will not control stock`}
+ onClick={(): void => {
+ valueHandler({
+ current: 0,
+ lost: 0,
+ sold: 0,
+ } as Stock as any);
+ }}
+ >
+ <span>
+ <i18n.Translate>Manage stock</i18n.Translate>
+ </span>
+ </button>
+ ) : (
+ <button
+ class="button"
+ data-tooltip={i18n.str`this product has been configured without stock control`}
+ disabled
+ >
+ <span>
+ <i18n.Translate>Infinite</i18n.Translate>
+ </span>
+ </button>
+ )}
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+
+ const currentStock =
+ (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0);
+
+ const stockAddedErrors: FormErrors<typeof addedStock> = {
+ lost:
+ currentStock + addedStock.incoming < addedStock.lost
+ ? i18n.str`lost cannot be greater than current and incoming (max ${
+ currentStock + addedStock.incoming
+ })`
+ : undefined,
+ };
+
+ // const stockUpdateDescription = stockAddedErrors.lost ? '' : (
+ // !!addedStock.incoming || !!addedStock.lost ?
+ // i18n.str`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` :
+ // i18n.str`current stock will stay at ${currentStock}`
+ // )
+
+ return (
+ <Fragment>
+ <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && (
+ <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </p>
+ </header>
+ <div class="card-content">
+ <FormProvider<Entity>
+ name="stock"
+ errors={errors}
+ object={formValue}
+ valueHandler={valueHandler}
+ >
+ {alreadyExist ? (
+ <Fragment>
+ <FormProvider
+ name="added"
+ errors={stockAddedErrors}
+ object={addedStock}
+ valueHandler={setAddedStock as any}
+ >
+ <InputNumber name="incoming" label={i18n.str`Incoming`} />
+ <InputNumber name="lost" label={i18n.str`Lost`} />
+ </FormProvider>
+
+ {/* <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ {stockUpdateDescription}
+ </div>
+ </div>
+ </div> */}
+ </Fragment>
+ ) : (
+ <InputNumber<Entity>
+ name="current"
+ label={i18n.str`Current`}
+ side={
+ <button
+ class="button is-danger"
+ data-tooltip={i18n.str`remove stock control for this product`}
+ onClick={(): void => {
+ valueHandler(undefined as any);
+ }}
+ >
+ <span>
+ <i18n.Translate>without stock</i18n.Translate>
+ </span>
+ </button>
+ }
+ />
+ )}
+
+ <InputDate<Entity>
+ name="nextRestock"
+ label={i18n.str`Next restock`}
+ withTimestampSupport
+ />
+
+ <InputGroup<Entity> name="address" label={i18n.str`Warehouse address`}>
+ <InputLocation name="address" />
+ </InputGroup>
+ </FormProvider>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+// (
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx
new file mode 100644
index 000000000..2701768aa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx
@@ -0,0 +1,90 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ values: any[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputTab<T>({
+ name,
+ readonly,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ values,
+ fromStr = defaultFromString,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-icons-right">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ {values.map((v, i) => {
+ return (
+ <li key={i} class={value === v ? "is-active" : ""}
+ onClick={(e) => { onChange(v) }}
+ >
+ <a style={{ cursor: "initial" }}>
+ <span>{toStr(v)}</span>
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ {help}
+ </p>
+ {required && (
+ <span class="icon has-text-danger is-right" style={{ height: "2.5em" }}>
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx
new file mode 100644
index 000000000..b5722e4ec
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx
@@ -0,0 +1,147 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useCallback, useState } from "preact/hooks";
+import * as yup from "yup";
+import { MerchantBackend } from "../../declaration.js";
+import { TaxSchema as schema } from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { Input } from "./Input.js";
+import { InputGroup } from "./InputGroup.js";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+type Entity = MerchantBackend.Tax;
+export function InputTaxes<T>({
+ name,
+ readonly,
+ label,
+}: Props<keyof T>): VNode {
+ const { value: taxes, onChange } = useField<T>(name);
+
+ const [value, valueHandler] = useState<Partial<Entity>>({});
+ // const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ schema.validateSync(value, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): void => {
+ onChange([value as any, ...taxes] as any);
+ valueHandler({});
+ }, [value]);
+
+ const { i18n } = useTranslationContext();
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup
+ name="tax"
+ label={label}
+ alternative={
+ taxes.length > 0 && (
+ <p>This product has {taxes.length} applicable taxes configured.</p>
+ )
+ }
+ >
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body" style={{ display: "block" }}>
+ {taxes.map((v: any, i: number) => (
+ <div
+ key={i}
+ class="tags has-addons mt-3 mb-0 mr-3"
+ style={{ flexWrap: "nowrap" }}
+ >
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ <b>{v.tax}</b>: {v.name}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(taxes.filter((f: any) => f !== v) as any);
+ valueHandler(v);
+ }}
+ />
+ </div>
+ ))}
+ {!taxes.length && i18n.str`No taxes configured for this product.`}
+ </div>
+ </div>
+
+ <Input<Entity>
+ name="tax"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`Taxes can be in currencies that differ from the main currency used by the merchant.`}
+ >
+ <i18n.Translate>
+ Enter currency and value separated with a colon, e.g.
+ &quot;USD:2.3&quot;.
+ </i18n.Translate>
+ </Input>
+
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Legal name of the tax, e.g. VAT or import duties.`}
+ />
+
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`add tax to the tax list`}
+ disabled={hasErrors}
+ onClick={submit}
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </button>
+ </div>
+ </FormProvider>
+ </InputGroup>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
new file mode 100644
index 000000000..f95dfcd05
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
@@ -0,0 +1,91 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputToggle<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label" >
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <div class="toggle-switch"></div>
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
new file mode 100644
index 000000000..e9fd88770
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -0,0 +1,116 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { InputProps, useField } from "./useField.js";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ inputType?: "text" | "number" | "password";
+ addonBefore?: ComponentChildren;
+ addonAfter?: ComponentChildren;
+ addonAfterAction?: () => void;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any;
+ children?: ComponentChildren;
+ side?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputWithAddon<T>({
+ name,
+ readonly,
+ addonBefore,
+ children,
+ expand,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ inputType,
+ inputExtra,
+ side,
+ addonAfter,
+ addonAfterAction,
+ toStr = defaultToString,
+ fromStr = defaultFromString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" 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">
+ {addonBefore && (
+ <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>
+ )}
+ <p
+ class={`control${expand ? " is-expanded" : ""}${required ? " has-icons-right" : ""
+ }`}
+ >
+ <input
+ {...(inputExtra || {})}
+ class={error ? "input is-danger" : "input"}
+ type={inputType}
+ placeholder={placeholder}
+ readonly={readonly}
+ disabled={readonly}
+ name={String(name)}
+ value={toStr(value)}
+ onChange={(e): void => onChange(fromStr(e.currentTarget.value))}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {children}
+ </p>
+ {addonAfter && (
+ <div class="control" onClick={addonAfterAction} style={{ cursor: addonAfterAction ? "pointer" : undefined }}>
+ <a class="button is-static">{addonAfter}</a>
+ </div>
+ )}
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ <span class="has-text-grey">{help}</span>
+ </div>
+ {expand ? <div>{side}</div> : side}
+ </div>
+
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx
new file mode 100644
index 000000000..2ff23dfd3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx
@@ -0,0 +1,59 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+
+export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode {
+ const { i18n } = useTranslationContext()
+
+ const [error, setError] = useState<string | undefined>(
+ undefined,
+ );
+
+ const [id, setId] = useState<string>()
+ async function check(currentId: string | undefined): Promise<void> {
+ if (!currentId) {
+ setError(i18n.str`missing id`);
+ return;
+ }
+ try {
+ await testIfExist(currentId);
+ onSelect(currentId);
+ setError(undefined);
+ } catch {
+ setError(i18n.str`not found`);
+ }
+ }
+
+ return <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <div class="field has-addons">
+ <div class="control">
+ <input
+ class={error ? "input is-danger" : "input"}
+ type="text"
+ value={id ?? ""}
+ onChange={(e) => setId(e.currentTarget.value)}
+ placeholder={palceholder}
+ />
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={description}
+ >
+ <button
+ class="button"
+ onClick={(e) => check(id)}
+ >
+ <span class="icon">
+ <i class="mdi mdi-arrow-right" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/TextField.tsx b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx
new file mode 100644
index 000000000..03f36dcbb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/TextField.tsx
@@ -0,0 +1,71 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: "text" | "number" | "multiline" | "password";
+ expand?: boolean;
+ side?: ComponentChildren;
+ children: ComponentChildren;
+}
+
+export function TextField<T>({
+ name,
+ tooltip,
+ label,
+ expand,
+ help,
+ children,
+ side,
+}: Props<keyof T>): VNode {
+ const { error } = useField<T>(name);
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ {children}
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/useField.tsx b/packages/auditor-backoffice-ui/src/components/form/useField.tsx
new file mode 100644
index 000000000..c7559faae
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/useField.tsx
@@ -0,0 +1,92 @@
+/*
+ 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)
+ */
+
+import { ComponentChildren, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useFormContext } from "./FormProvider.js";
+
+interface Use<V> {
+ error?: string;
+ required: boolean;
+ value: any;
+ initial: any;
+ onChange: (v: V) => void;
+ toStr: (f: V | undefined) => string;
+ fromStr: (v: string) => V;
+}
+
+export function useField<T>(name: keyof T): Use<T[typeof name]> {
+ const { errors, object, initialObject, toStr, fromStr, valueHandler } =
+ useFormContext<T>();
+ type P = typeof name;
+ type V = T[P];
+ const [isDirty, setDirty] = useState(false);
+ const updateField =
+ (field: P) =>
+ (value: V): void => {
+ setDirty(true);
+ return valueHandler((prev) => {
+ return setValueDeeper(prev, String(field).split("."), value);
+ });
+ };
+
+ 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 hasError = readField(errors, String(name));
+ return {
+ error: isDirty ? hasError : undefined,
+ required: !isDirty && hasError,
+ value,
+ initial,
+ onChange: updateField(name) as any,
+ toStr: toStr[name] ? toStr[name]! : defaultToString,
+ fromStr: fromStr[name] ? fromStr[name]! : defaultFromString,
+ };
+}
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+const readField = (object: any, name: string) => {
+ return name
+ .split(".")
+ .reduce((prev, current) => prev && prev[current], object);
+};
+
+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) };
+};
+
+export interface InputProps<T> {
+ name: T;
+ label: ComponentChildren;
+ placeholder?: string;
+ tooltip?: ComponentChildren;
+ readonly?: boolean;
+ help?: ComponentChildren;
+}
diff --git a/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx
new file mode 100644
index 000000000..9a445eb32
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx
@@ -0,0 +1,41 @@
+/*
+ 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)
+ */
+
+import { useFormContext } from "./FormProvider.js";
+
+interface Use {
+ hasError?: boolean;
+}
+
+export function useGroupField<T>(name: keyof T): Use {
+ const f = useFormContext<T>();
+ if (!f) return {};
+
+ return {
+ 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/auditor-backoffice-ui/src/components/index.stories.ts b/packages/auditor-backoffice-ui/src/components/index.stories.ts
new file mode 100644
index 000000000..c57ddab14
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/index.stories.ts
@@ -0,0 +1,17 @@
+/*
+ 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/>
+ */
+
+export * as payto from "./form/InputPaytoForm.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
new file mode 100644
index 000000000..6f5881fc0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -0,0 +1,124 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../context/backend.js";
+import { Entity } from "../../paths/admin/create/CreatePage.js";
+import { Input } from "../form/Input.js";
+import { InputDuration } from "../form/InputDuration.js";
+import { InputGroup } from "../form/InputGroup.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputLocation } from "../form/InputLocation.js";
+import { InputSelector } from "../form/InputSelector.js";
+import { InputToggle } from "../form/InputToggle.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+
+export function DefaultInstanceFormFields({
+ readonlyId,
+ showId,
+}: {
+ readonlyId?: boolean;
+ showId: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ return (
+ <Fragment>
+ {showId && (
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`${backendURL}/instances/`}
+ readonly={readonlyId}
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
+ />
+ )}
+
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Business name`}
+ tooltip={i18n.str`Legal name of the business represented by this instance.`}
+ />
+
+ <InputSelector<Entity>
+ name="user_type"
+ label={i18n.str`Type`}
+ tooltip={i18n.str`Different type of account can have different rules and requirements.`}
+ values={["business", "individual"]}
+ />
+
+ <Input<Entity>
+ name="email"
+ label={i18n.str`Email`}
+ tooltip={i18n.str`Contact email`}
+ />
+
+ <Input<Entity>
+ name="website"
+ label={i18n.str`Website URL`}
+ tooltip={i18n.str`URL.`}
+ />
+
+ <InputImage<Entity>
+ name="logo"
+ label={i18n.str`Logo`}
+ tooltip={i18n.str`Logo image.`}
+ />
+
+ <InputToggle<Entity>
+ name="use_stefan"
+ label={i18n.str`Pay transaction fee`}
+ tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
+ />
+
+ <InputGroup
+ name="address"
+ label={i18n.str`Address`}
+ tooltip={i18n.str`Physical location of the merchant.`}
+ >
+ <InputLocation name="address" />
+ </InputGroup>
+
+ <InputGroup
+ name="jurisdiction"
+ label={i18n.str`Jurisdiction`}
+ tooltip={i18n.str`Jurisdiction for legal disputes with the merchant.`}
+ >
+ <InputLocation name="jurisdiction" />
+ </InputGroup>
+
+ <InputDuration<Entity>
+ name="default_pay_delay"
+ label={i18n.str`Default payment delay`}
+ withForever
+ tooltip={i18n.str`Time customers have to pay an order before the offer expires by default.`}
+ />
+
+ <InputDuration<Entity>
+ name="default_wire_transfer_delay"
+ label={i18n.str`Default wire transfer delay`}
+ tooltip={i18n.str`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.`}
+ withForever
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx
new file mode 100644
index 000000000..41fe1374a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx
@@ -0,0 +1,92 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import langIcon from "../../assets/icons/languageicon.svg";
+import { strings as messages } from "../../i18n/strings.js";
+
+type LangsNames = {
+ [P in keyof typeof messages]: string;
+};
+
+const names: LangsNames = {
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
+
+function getLangName(s: keyof LangsNames | string) {
+ if (names[s]) return names[s];
+ return s;
+}
+
+export function LangSelector(): VNode {
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage } = useTranslationContext();
+
+ return (
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
+ {Object.keys(messages)
+ .filter((l) => l !== lang)
+ .map((l) => (
+ <a
+ key={l}
+ class="dropdown-item"
+ value={l}
+ onClick={() => {
+ changeLanguage(l);
+ setUpdatingLang(false);
+ }}
+ >
+ {getLangName(l)}
+ </a>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
new file mode 100644
index 000000000..9f1b33893
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
@@ -0,0 +1,72 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import logo from "../../assets/logo-2021.svg";
+
+interface Props {
+ onMobileMenu: () => void;
+ title: string;
+}
+
+export function NavigationBar({ onMobileMenu, title }: Props): VNode {
+ return (
+ <nav
+ class="navbar is-fixed-top"
+ role="navigation"
+ aria-label="main navigation"
+ >
+ <div class="navbar-brand">
+ <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
+ {title}
+ </span>
+
+ <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a>
+ </div>
+
+ <div class="navbar-menu ">
+ <a
+ class="navbar-start is-justify-content-center is-flex-grow-1"
+ href="https://taler.net"
+ >
+ <img src={logo} style={{ height: 35, margin: 10 }} />
+ </a>
+ <div class="navbar-end">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+ </div>
+ </div>
+ </div>
+ </nav>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
new file mode 100644
index 000000000..cfc00148e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
@@ -0,0 +1,284 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../context/backend.js";
+import { useConfigContext } from "../../context/config.js";
+import { useInstanceKYCDetails } from "../../hooks/instance.js";
+import { LangSelector } from "./LangSelector.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+interface Props {
+ onLogout: () => void;
+ onShowSettings: () => void;
+ mobile?: boolean;
+ instance: string;
+ admin?: boolean;
+ mimic?: boolean;
+ isPasswordOk: boolean;
+}
+
+export function Sidebar({
+ mobile,
+ instance,
+ onShowSettings,
+ onLogout,
+ admin,
+ mimic,
+ isPasswordOk
+}: Props): VNode {
+ const config = useConfigContext();
+ const { url: backendURL } = useBackendContext()
+ const { i18n } = useTranslationContext();
+ const kycStatus = useInstanceKYCDetails();
+ const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
+
+ return (
+ <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
+ {mobile && (
+ <div
+ class="footer"
+ onClick={(e) => {
+ return e.stopImmediatePropagation();
+ }}
+ >
+ <LangSelector />
+ </div>
+ )}
+ <div class="aside-tools">
+ <div class="aside-tools-label">
+ <div>
+ <b>Taler</b> Backoffice
+ </div>
+ <div
+ class="is-size-7 has-text-right"
+ style={{ lineHeight: 0, marginTop: -10 }}
+ >
+ {VERSION} ({config.version})
+ </div>
+ </div>
+ </div>
+ <div class="menu is-menu-main">
+ {instance ? (
+ <Fragment>
+ <ul class="menu-list">
+ <li>
+ <a href={"/orders"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Orders</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/inventory"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Inventory</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/transfers"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-arrow-left-right" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Transfers</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/templates"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Templates</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ {needKYC && (
+ <li>
+ <a href={"/kyc"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-account-check" />
+ </span>
+ <span class="menu-item-label">KYC Status</span>
+ </a>
+ </li>
+ )}
+ </ul>
+ <p class="menu-label">
+ <i18n.Translate>Configuration</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a href={"/bank"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-bank" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Bank account</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/otp-devices"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-lock" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/reserves"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <span class="menu-item-label">Reserves</span>
+ </a>
+ </li>
+ <li>
+ <a href={"/webhooks"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/settings"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Settings</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/token"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-security" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </Fragment>
+ ) : undefined}
+ <p class="menu-label">
+ <i18n.Translate>Connection</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onShowSettings()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Interface</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <div>
+ <span style={{ width: "3rem" }} class="icon">
+ <i class="mdi mdi-web" />
+ </span>
+ <span class="menu-item-label">
+ {new URL(backendURL).hostname}
+ </span>
+ </div>
+ </li>
+ <li>
+ <div>
+ <span style={{ width: "3rem" }} class="icon">
+ ID
+ </span>
+ <span class="menu-item-label">
+ {!instance ? "default" : instance}
+ </span>
+ </div>
+ </li>
+ {admin && !mimic && (
+ <Fragment>
+ <p class="menu-label">
+ <i18n.Translate>Instances</i18n.Translate>
+ </p>
+ <li>
+ <a href={"/instance/new"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-plus" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>New</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/instances"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-format-list-bulleted" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>List</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </Fragment>
+ )}
+ {isPasswordOk ?
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onLogout()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Log out</i18n.Translate>
+ </span>
+ </a>
+ </li> : undefined
+ }
+ </ul>
+ </div>
+ </aside>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
new file mode 100644
index 000000000..03ae3b005
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
@@ -0,0 +1,269 @@
+/*
+ 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/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { AdminPaths } from "../../AdminRoutes.js";
+import { InstancePaths } from "../../InstanceRoutes.js";
+import { Notification } from "../../utils/types.js";
+import { NavigationBar } from "./NavigationBar.js";
+import { Sidebar } from "./SideBar.js";
+
+function getInstanceTitle(path: string, id: string): string {
+ switch (path) {
+ case InstancePaths.settings:
+ return `${id}: Settings`;
+ case InstancePaths.order_list:
+ return `${id}: Orders`;
+ case InstancePaths.order_new:
+ return `${id}: New order`;
+ case InstancePaths.inventory_list:
+ return `${id}: Inventory`;
+ case InstancePaths.deposit_confirmation_list:
+ return `${id}: Deposit Confirmation`;
+ case InstancePaths.inventory_new:
+ return `${id}: New product`;
+ case InstancePaths.inventory_update:
+ return `${id}: Update product`;
+ case InstancePaths.reserves_new:
+ return `${id}: New reserve`;
+ case InstancePaths.reserves_list:
+ return `${id}: Reserves`;
+ case InstancePaths.transfers_list:
+ return `${id}: Transfers`;
+ case InstancePaths.transfers_new:
+ return `${id}: New transfer`;
+ case InstancePaths.webhooks_list:
+ return `${id}: Webhooks`;
+ case InstancePaths.webhooks_new:
+ return `${id}: New webhook`;
+ case InstancePaths.webhooks_update:
+ return `${id}: Update webhook`;
+ case InstancePaths.otp_devices_list:
+ return `${id}: otp devices`;
+ case InstancePaths.otp_devices_new:
+ return `${id}: New otp devices`;
+ case InstancePaths.otp_devices_update:
+ return `${id}: Update otp devices`;
+ case InstancePaths.templates_new:
+ return `${id}: New template`;
+ case InstancePaths.templates_update:
+ return `${id}: Update template`;
+ case InstancePaths.templates_list:
+ return `${id}: Templates`;
+ case InstancePaths.templates_use:
+ return `${id}: Use template`;
+ case InstancePaths.interface:
+ return `${id}: Interface`;
+ default:
+ return "";
+ }
+}
+
+function getAdminTitle(path: string, instance: string) {
+ if (path === AdminPaths.new_instance) return `New instance`;
+ if (path === AdminPaths.list_instances) return `Instances`;
+ return getInstanceTitle(path, instance);
+}
+
+interface MenuProps {
+ title?: string;
+ path: string;
+ instance: string;
+ admin?: boolean;
+ onLogout?: () => void;
+ onShowSettings: () => void;
+ setInstanceName: (s: string) => void;
+ isPasswordOk: boolean;
+}
+
+function WithTitle({
+ title,
+ children,
+}: {
+ title: string;
+ children: ComponentChildren;
+}): VNode {
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
+}
+
+export function Menu({
+ onLogout,
+ onShowSettings,
+ title,
+ instance,
+ path,
+ admin,
+ setInstanceName,
+ isPasswordOk
+}: MenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ const titleWithSubtitle = title
+ ? title
+ : !admin
+ ? getInstanceTitle(path, instance)
+ : getAdminTitle(path, instance);
+ const adminInstance = instance === "default";
+ const mimic = admin && !adminInstance;
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ {onLogout && (
+ <Sidebar
+ onShowSettings={onShowSettings}
+ onLogout={onLogout}
+ admin={admin}
+ mimic={mimic}
+ instance={instance}
+ mobile={mobileOpen}
+ isPasswordOk={isPasswordOk}
+ />
+ )}
+
+ {mimic && (
+ <nav class="level" style={{
+ zIndex: 100,
+ position: "fixed",
+ width: "50%",
+ marginLeft: "20%"
+ }}>
+ <div class="level-item has-text-centered has-background-warning">
+ <p class="is-size-5">
+ You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
+ <a
+ href="#/instances"
+ onClick={(e) => {
+ setInstanceName("default");
+ }}
+ >
+ go back
+ </a>
+ </p>
+ </div>
+ </nav>
+ )}
+ </div>
+ </WithTitle>
+ );
+}
+
+interface NotYetReadyAppMenuProps {
+ title: string;
+ onShowSettings: () => void;
+ onLogout?: () => void;
+ isPasswordOk: boolean;
+}
+
+interface NotifProps {
+ notification?: Notification;
+}
+export function NotificationCard({
+ notification: n,
+}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && (
+ <div class="message-body">
+ <div>{n.description}</div>
+ {n.details && <pre>{n.details}</pre>}
+ </div>
+ )}
+ </article>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+interface NotConnectedAppMenuProps {
+ title: string;
+}
+export function NotConnectedAppMenu({
+ title,
+}: NotConnectedAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ </div>
+ );
+}
+
+export function NotYetReadyAppMenu({
+ onLogout,
+ onShowSettings,
+ title,
+ isPasswordOk
+}: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ {onLogout && (
+ <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
+ )}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
new file mode 100644
index 000000000..8372c84cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
@@ -0,0 +1,496 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useInstanceContext } from "../../context/instance.js";
+import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
+import { Spinner } from "../exception/loading.js";
+import { FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+
+interface Props {
+ active?: boolean;
+ description?: string;
+ onCancel?: () => void;
+ onConfirm?: () => void;
+ label?: string;
+ children?: ComponentChildren;
+ danger?: boolean;
+ disabled?: boolean;
+}
+
+export function ConfirmModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ danger,
+ disabled,
+ label = "Confirm",
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card" style={{ maxWidth: 700 }}>
+ <header class="modal-card-head">
+ {!description ? null : (
+ <p class="modal-card-title">
+ <b>{description}</b>
+ </p>
+ )}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ {onConfirm ? (
+ <Fragment>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button
+ class={danger ? "button is-danger " : "button is-info "}
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>{label}</i18n.Translate>
+ </button>
+ </Fragment>
+ ) : (
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function ContinueModal({
+ active,
+ description,
+ onCancel,
+ onConfirm,
+ children,
+ disabled,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class={active ? "modal is-active" : "modal"}>
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head has-background-success">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button
+ class="button is-success "
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </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 {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ {!description ? null : <p class="modal-card-title">{description}</p>}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">{children}</section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Clear</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={onConfirm}
+ disabled={onConfirm === undefined}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+interface DeleteModalProps {
+ element: { id: string; name: string };
+ onCancel: () => void;
+ onConfirm: (id: string) => void;
+}
+
+export function DeleteModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Delete instance`}
+ description={`Delete the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you delete the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), the merchant will no longer be able to process
+ orders or refunds
+ </p>
+ <p>
+ This action deletes the instance private key, but preserves all
+ transaction data. You can still access that data after deleting the
+ instance.
+ </p>
+ <p class="warning">
+ Deleting an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
+}
+
+export function PurgeModal({
+ element,
+ onCancel,
+ onConfirm,
+}: DeleteModalProps): VNode {
+ return (
+ <ConfirmModal
+ label={`Purge the instance`}
+ description={`Purge the instance "${element.name}"`}
+ danger
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(element.id)}
+ >
+ <p>
+ If you purge the instance named <b>&quot;{element.name}&quot;</b> (ID:{" "}
+ <b>{element.id}</b>), you will also delete all it&apos;s transaction
+ data.
+ </p>
+ <p>
+ The instance will disappear from your list, and you will no longer be
+ able to access it&apos;s data.
+ </p>
+ <p class="warning">
+ Purging an instance <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ );
+}
+
+interface UpdateTokenModalProps {
+ oldToken?: string;
+ onCancel: () => void;
+ onConfirm: (value: string) => void;
+ onClear: () => void;
+}
+
+//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal
+export function UpdateTokenModal({
+ onCancel,
+ onClear,
+ onConfirm,
+ oldToken,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token;
+ const errors = {
+ old_token: hasInputTheCorrectOldToken
+ ? i18n.str`is not the same as the current access token`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
+
+ return (
+ <ClearConfirmModal
+ description={text}
+ onCancel={onCancel}
+ onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined}
+ onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined}
+ >
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ {oldToken && (
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Old access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ )}
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
+ </div>
+ </ClearConfirmModal>
+ );
+}
+
+export function SetTokenNewInstanceModal({
+ onCancel,
+ onClear,
+ onConfirm,
+}: UpdateTokenModalProps): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const errors = {
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old access token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">{i18n.str`You are setting the access token for the new instance`}</p>
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </FormProvider>
+ <p>
+ <i18n.Translate>
+ With external authorization method no check will be done by
+ the merchant backend
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ <footer class="modal-card-foot">
+ {onClear && (
+ <button
+ class="button is-danger"
+ onClick={onClear}
+ disabled={onClear === undefined}
+ >
+ <i18n.Translate>Set external authorization</i18n.Translate>
+ </button>
+ )}
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info"
+ onClick={() => onConfirm(form.new_token!)}
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Set access token</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
+
+export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">
+ <i18n.Translate>Operation in progress...</i18n.Translate>
+ </p>
+ </header>
+ <section class="modal-card-body">
+ <div class="columns">
+ <div class="column" />
+ <Spinner />
+ <div class="column" />
+ </div>
+ <p>{i18n.str`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p>
+ </section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..073382fb1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
@@ -0,0 +1,57 @@
+/*
+ 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)
+ */
+import { ComponentChildren, h, VNode } from "preact";
+
+interface Props {
+ onCreateAnother?: () => void;
+ onConfirm: () => void;
+ children: ComponentChildren;
+}
+
+export function CreatedSuccessfully({
+ children,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <div class="columns is-fullwidth is-vcentered mt-3">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="card">
+ <header class="card-header has-background-success">
+ <p class="card-header-title has-text-white-ter">Success.</p>
+ </header>
+ <div class="card-content">{children}</div>
+ </div>
+ <div class="buttons is-right">
+ {onCreateAnother && (
+ <button class="button is-info" onClick={onCreateAnother}>
+ Create another
+ </button>
+ )}
+ <button class="button is-info" onClick={onConfirm}>
+ Continue
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx
new file mode 100644
index 000000000..af594de0f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ 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)
+ */
+
+import { h } from "preact";
+import { Notifications } from "./index.js";
+
+export default {
+ title: "Components/Notification",
+ component: Notifications,
+ argTypes: {
+ removeNotification: { action: "removeNotification" },
+ },
+};
+
+export const Info = (a: any) => <Notifications {...a} />;
+Info.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "INFO",
+ },
+ ],
+};
+export const Warn = (a: any) => <Notifications {...a} />;
+Warn.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "WARN",
+ },
+ ],
+};
+export const Error = (a: any) => <Notifications {...a} />;
+Error.args = {
+ notifications: [
+ {
+ message: "Title",
+ description: "Some large description",
+ type: "ERROR",
+ },
+ ],
+};
diff --git a/packages/auditor-backoffice-ui/src/components/notifications/index.tsx b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx
new file mode 100644
index 000000000..235c75577
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/notifications/index.tsx
@@ -0,0 +1,65 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { MessageType, Notification } from "../../utils/types.js";
+
+interface Props {
+ notifications: Notification[];
+ removeNotification?: (n: Notification) => void;
+}
+
+function messageStyle(type: MessageType): string {
+ switch (type) {
+ case "INFO":
+ return "message is-info";
+ case "WARN":
+ return "message is-warning";
+ case "ERROR":
+ return "message is-danger";
+ case "SUCCESS":
+ return "message is-success";
+ default:
+ return "message";
+ }
+}
+
+export function Notifications({
+ notifications,
+ removeNotification,
+}: Props): VNode {
+ return (
+ <div class="toast">
+ {notifications.map((n, i) => (
+ <article key={i} class={messageStyle(n.type)}>
+ <div class="message-header">
+ <p>{n.message}</p>
+ <button
+ class="delete"
+ onClick={() => removeNotification && removeNotification(n)}
+ />
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ ))}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx
new file mode 100644
index 000000000..0bc629d46
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx
@@ -0,0 +1,349 @@
+/*
+ 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)
+ */
+
+import { h, Component } from "preact";
+
+interface Props {
+ closeFunction?: () => void;
+ dateReceiver?: (d: Date) => void;
+ opened?: boolean;
+}
+interface State {
+ displayedMonth: number;
+ displayedYear: number;
+ selectYearMode: boolean;
+ currentDate: Date;
+}
+
+// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
+export class DatePicker extends Component<Props, State> {
+ closeDatePicker() {
+ this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
+ }
+
+ /**
+ * Gets fired when a day gets clicked.
+ * @param {object} e The event thrown by the <span /> element clicked
+ */
+ dayClicked(e: any) {
+ const element = e.target; // the actual element clicked
+
+ if (element.innerHTML === "") return false; // don't continue if <span /> empty
+
+ // get date from clicked element (gets attached when rendered)
+ const date = new Date(element.getAttribute("data-value"));
+
+ // update the state
+ this.setState({ currentDate: date });
+ this.passDateToParent(date);
+ }
+
+ /**
+ * returns days in month as array
+ * @param {number} month the month to display
+ * @param {number} year the year to display
+ */
+ getDaysByMonth(month: number, year: number) {
+ const calendar = [];
+
+ const date = new Date(year, month, 1); // month to display
+
+ const firstDay = new Date(year, month, 1).getDay(); // first weekday of month
+ const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month
+
+ let day: number | null = 0;
+
+ // the calendar is 7*6 fields big, so 42 loops
+ for (let i = 0; i < 42; i++) {
+ if (i >= firstDay && day !== null) day = day + 1;
+ if (day !== null && day > lastDate) day = null;
+
+ // append the calendar Array
+ calendar.push({
+ day: day === 0 || day === null ? null : day, // null or number
+ date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date()
+ today:
+ day === now.getDate() &&
+ month === now.getMonth() &&
+ year === now.getFullYear(), // boolean
+ });
+ }
+
+ return calendar;
+ }
+
+ /**
+ * Display previous month by updating state
+ */
+ displayPrevMonth() {
+ if (this.state.displayedMonth <= 0) {
+ this.setState({
+ displayedMonth: 11,
+ displayedYear: this.state.displayedYear - 1,
+ });
+ } else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth - 1,
+ });
+ }
+ }
+
+ /**
+ * Display next month by updating state
+ */
+ displayNextMonth() {
+ if (this.state.displayedMonth >= 11) {
+ this.setState({
+ displayedMonth: 0,
+ displayedYear: this.state.displayedYear + 1,
+ });
+ } else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth + 1,
+ });
+ }
+ }
+
+ /**
+ * Display the selected month (gets fired when clicking on the date string)
+ */
+ displaySelectedMonth() {
+ if (this.state.selectYearMode) {
+ this.toggleYearSelector();
+ } else {
+ if (!this.state.currentDate) return false;
+ this.setState({
+ displayedMonth: this.state.currentDate.getMonth(),
+ displayedYear: this.state.currentDate.getFullYear(),
+ });
+ }
+ }
+
+ toggleYearSelector() {
+ this.setState({ selectYearMode: !this.state.selectYearMode });
+ }
+
+ changeDisplayedYear(e: any) {
+ const element = e.target;
+ this.toggleYearSelector();
+ this.setState({
+ displayedYear: parseInt(element.innerHTML, 10),
+ displayedMonth: 0,
+ });
+ }
+
+ /**
+ * Pass the selected date to parent when 'OK' is clicked
+ */
+ passSavedDateDateToParent() {
+ this.passDateToParent(this.state.currentDate);
+ }
+ passDateToParent(date: Date) {
+ if (typeof this.props.dateReceiver === "function")
+ this.props.dateReceiver(date);
+ this.closeDatePicker();
+ }
+
+ componentDidUpdate() {
+ if (this.state.selectYearMode) {
+ document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
+ }
+ }
+
+ constructor() {
+ super();
+
+ this.closeDatePicker = this.closeDatePicker.bind(this);
+ this.dayClicked = this.dayClicked.bind(this);
+ this.displayNextMonth = this.displayNextMonth.bind(this);
+ this.displayPrevMonth = this.displayPrevMonth.bind(this);
+ this.getDaysByMonth = this.getDaysByMonth.bind(this);
+ this.changeDisplayedYear = this.changeDisplayedYear.bind(this);
+ this.passDateToParent = this.passDateToParent.bind(this);
+ this.toggleYearSelector = this.toggleYearSelector.bind(this);
+ this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
+
+ this.state = {
+ currentDate: now,
+ displayedMonth: now.getMonth(),
+ displayedYear: now.getFullYear(),
+ selectYearMode: false,
+ };
+ }
+
+ render() {
+ const { currentDate, displayedMonth, displayedYear, selectYearMode } =
+ this.state;
+
+ return (
+ <div>
+ <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
+ <div class="datePicker--titles">
+ <h3
+ style={{
+ color: selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.toggleYearSelector}
+ >
+ {currentDate.getFullYear()}
+ </h3>
+ <h2
+ style={{
+ color: !selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.displaySelectedMonth}
+ >
+ {dayArr[currentDate.getDay()]},{" "}
+ {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
+ </h2>
+ </div>
+
+ {!selectYearMode && (
+ <nav>
+ <span onClick={this.displayPrevMonth} class="icon">
+ <i
+ style={{ transform: "rotate(180deg)" }}
+ class="mdi mdi-forward"
+ />
+ </span>
+ <h4>
+ {monthArrShortFull[displayedMonth]} {displayedYear}
+ </h4>
+ <span onClick={this.displayNextMonth} class="icon">
+ <i class="mdi mdi-forward" />
+ </span>
+ </nav>
+ )}
+
+ <div class="datePicker--scroll">
+ {!selectYearMode && (
+ <div class="datePicker--calendar">
+ <div class="datePicker--dayNames">
+ {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
+ <span key={i}>{day}</span>
+ ))}
+ </div>
+
+ <div onClick={this.dayClicked} class="datePicker--days">
+ {/*
+ Loop through the calendar object returned by getDaysByMonth().
+ */}
+
+ {this.getDaysByMonth(
+ this.state.displayedMonth,
+ this.state.displayedYear,
+ ).map((day) => {
+ let selected = false;
+
+ if (currentDate && day.date)
+ selected =
+ currentDate.toLocaleDateString() ===
+ day.date.toLocaleDateString();
+
+ return (
+ <span
+ key={day.day}
+ class={
+ (day.today ? "datePicker--today " : "") +
+ (selected ? "datePicker--selected" : "")
+ }
+ disabled={!day.date}
+ data-value={day.date}
+ >
+ {day.day}
+ </span>
+ );
+ })}
+ </div>
+ </div>
+ )}
+
+ {selectYearMode && (
+ <div class="datePicker--selectYear">
+ {yearArr.map((year) => (
+ <span
+ key={year}
+ class={year === displayedYear ? "selected" : ""}
+ onClick={this.changeDisplayedYear}
+ >
+ {year}
+ </span>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div
+ class="datePicker--background"
+ onClick={this.closeDatePicker}
+ style={{
+ display: this.props.opened ? "block" : "none",
+ }}
+ />
+ </div>
+ );
+ }
+}
+
+const monthArrShortFull = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+const monthArrShort = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+];
+
+const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+const now = new Date();
+
+const yearArr: number[] = [];
+
+for (let i = 2010; i <= now.getFullYear() + 10; i++) {
+ yearArr.push(i);
+}
diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
new file mode 100644
index 000000000..8f74d55ac
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
@@ -0,0 +1,55 @@
+/*
+ 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)
+ */
+
+import { h, FunctionalComponent } from "preact";
+import { useState } from "preact/hooks";
+import { DurationPicker as TestedComponent } from "./DurationPicker.js";
+
+export default {
+ title: "Components/Picker/Duration",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ days: true,
+ minutes: true,
+ hours: true,
+ seconds: true,
+ value: 10000000,
+});
+
+export const WithState = () => {
+ const [v, s] = useState<number>(1000000);
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
+};
diff --git a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx
new file mode 100644
index 000000000..ba003cce5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx
@@ -0,0 +1,211 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import "../../scss/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 ss = 1000;
+ const ms = ss * 60;
+ const hs = ms * 60;
+ const ds = hs * 24;
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="rdp-picker">
+ {days && (
+ <DurationColumn
+ unit={i18n.str`days`}
+ max={99}
+ value={Math.floor(value / ds)}
+ onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
+ onChange={(diff) => onChange(value + diff * ds)}
+ />
+ )}
+ {hours && (
+ <DurationColumn
+ unit={i18n.str`hours`}
+ max={23}
+ min={1}
+ value={Math.floor(value / hs) % 24}
+ onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
+ onChange={(diff) => onChange(value + diff * hs)}
+ />
+ )}
+ {minutes && (
+ <DurationColumn
+ unit={i18n.str`minutes`}
+ max={59}
+ min={1}
+ value={Math.floor(value / ms) % 60}
+ onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
+ onChange={(diff) => onChange(value + diff * ms)}
+ />
+ )}
+ {seconds && (
+ <DurationColumn
+ unit={i18n.str`seconds`}
+ max={59}
+ value={Math.floor(value / ss) % 60}
+ onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
+ onChange={(diff) => onChange(value + diff * ss)}
+ />
+ )}
+ </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 - 2}>
+ {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 + 2}>
+ {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}`;
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
new file mode 100644
index 000000000..2d5a54cde
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js";
+
+export default {
+ title: "Components/Product/Add",
+ component: TestedComponent,
+ argTypes: {
+ onAddProduct: { action: "onAddProduct" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithASimpleList = createExample(TestedComponent, {
+ inventory: [
+ {
+ id: "this id",
+ description: "this is the description",
+ } as any,
+ ],
+});
+
+export const WithAProductSelected = createExample(TestedComponent, {
+ inventory: [],
+ currentProducts: {
+ thisid: {
+ quantity: 1,
+ product: {
+ id: "asd",
+ description: "asdsadsad",
+ } as any,
+ },
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx
new file mode 100644
index 000000000..377d9c1ba
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -0,0 +1,127 @@
+/*
+ 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/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../declaration.js";
+import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputSearchOnList } from "../form/InputSearchOnList.js";
+
+type Form = {
+ product: MerchantBackend.Products.ProductDetail & WithId;
+ quantity: number;
+};
+
+interface Props {
+ currentProducts: ProductMap;
+ onAddProduct: (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number,
+ ) => void;
+ inventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+}
+
+export function InventoryProductForm({
+ currentProducts,
+ onAddProduct,
+ inventory,
+}: Props): VNode {
+ const initialState = { quantity: 1 };
+ const [state, setState] = useState<Partial<Form>>(initialState);
+ const [errors, setErrors] = useState<FormErrors<Form>>({});
+
+ const { i18n } = useTranslationContext();
+
+ const productWithInfiniteStock =
+ state.product && state.product.total_stock === -1;
+
+ const submit = (): void => {
+ if (!state.product) {
+ setErrors({
+ product: i18n.str`You must enter a valid product identifier.`,
+ });
+ return;
+ }
+ if (productWithInfiniteStock) {
+ onAddProduct(state.product, 1);
+ } else {
+ if (!state.quantity || state.quantity <= 0) {
+ setErrors({ quantity: i18n.str`Quantity must be greater than 0!` });
+ return;
+ }
+ const currentStock =
+ state.product.total_stock -
+ state.product.total_lost -
+ state.product.total_sold;
+ const p = currentProducts[state.product.id];
+ if (p) {
+ if (state.quantity + p.quantity > currentStock) {
+ const left = currentStock - p.quantity;
+ setErrors({
+ quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`,
+ });
+ return;
+ }
+ onAddProduct(state.product, state.quantity + p.quantity);
+ } else {
+ if (state.quantity > currentStock) {
+ const left = currentStock;
+ setErrors({
+ quantity: i18n.str`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`,
+ });
+ return;
+ }
+ onAddProduct(state.product, state.quantity);
+ }
+ }
+
+ setState(initialState);
+ };
+
+ return (
+ <FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
+ <InputSearchOnList
+ label={i18n.str`Search product`}
+ selected={state.product}
+ onChange={(p) => setState((v) => ({ ...v, product: p }))}
+ list={inventory}
+ withImage
+ />
+ {state.product && (
+ <div class="columns mt-5">
+ <div class="column is-two-thirds">
+ {!productWithInfiniteStock && (
+ <InputNumber<Form>
+ name="quantity"
+ label={i18n.str`Quantity`}
+ tooltip={i18n.str`how many products will be added`}
+ />
+ )}
+ </div>
+ <div class="column">
+ <div class="buttons is-right">
+ <button class="button is-success" onClick={submit}>
+ <i18n.Translate>Add from inventory</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ </FormProvider>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
new file mode 100644
index 000000000..c6d280f94
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
@@ -0,0 +1,215 @@
+/*
+ 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/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { MerchantBackend } from "../../declaration.js";
+import { useListener } from "../../hooks/listener.js";
+import { NonInventoryProductSchema as schema } from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputCurrency } from "../form/InputCurrency.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputTaxes } from "../form/InputTaxes.js";
+
+type Entity = MerchantBackend.Product;
+
+interface Props {
+ onAddProduct: (p: Entity) => Promise<void>;
+ productToEdit?: Entity;
+}
+export function NonInventoryProductFrom({
+ productToEdit,
+ onAddProduct,
+}: Props): VNode {
+ const [showCreateProduct, setShowCreateProduct] = useState(false);
+
+ const isEditing = !!productToEdit;
+
+ useEffect(() => {
+ setShowCreateProduct(isEditing);
+ }, [isEditing]);
+
+ const [submitForm, addFormSubmitter] = useListener<
+ Partial<MerchantBackend.Product> | undefined
+ >((result) => {
+ if (result) {
+ setShowCreateProduct(false);
+ return onAddProduct({
+ quantity: result.quantity || 0,
+ taxes: result.taxes || [],
+ description: result.description || "",
+ image: result.image || "",
+ price: result.price || "",
+ unit: result.unit || "",
+ });
+ }
+ return Promise.resolve();
+ });
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="buttons">
+ <button
+ class="button is-success"
+ data-tooltip={i18n.str`describe and add a product that is not in the inventory list`}
+ onClick={() => setShowCreateProduct(true)}
+ >
+ <i18n.Translate>Add custom product</i18n.Translate>
+ </button>
+ </div>
+ {showCreateProduct && (
+ <div class="modal is-active">
+ <div
+ class="modal-background "
+ onClick={() => setShowCreateProduct(false)}
+ />
+ <div class="modal-card">
+ <header class="modal-card-head">
+ <p class="modal-card-title">{i18n.str`Complete information of the product`}</p>
+ <button
+ class="delete "
+ aria-label="close"
+ onClick={() => setShowCreateProduct(false)}
+ />
+ </header>
+ <section class="modal-card-body">
+ <ProductForm
+ initial={productToEdit}
+ onSubscribe={addFormSubmitter}
+ />
+ </section>
+ <footer class="modal-card-foot">
+ <div class="buttons is-right" style={{ width: "100%" }}>
+ <button
+ class="button "
+ onClick={() => setShowCreateProduct(false)}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ class="button is-info "
+ disabled={!submitForm}
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={() => setShowCreateProduct(false)}
+ />
+ </div>
+ )}
+ </Fragment>
+ );
+}
+
+interface ProductProps {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+}
+
+interface NonInventoryProduct {
+ quantity: number;
+ description: string;
+ unit: string;
+ price: string;
+ image: string;
+ taxes: MerchantBackend.Tax[];
+}
+
+export function ProductForm({ onSubscribe, initial }: ProductProps): VNode {
+ const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({
+ taxes: [],
+ ...initial,
+ });
+ let errors: FormErrors<Entity> = {};
+ try {
+ schema.validateSync(value, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+
+ const submit = useCallback((): Entity | undefined => {
+ return value as MerchantBackend.Product;
+ }, [value]);
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <FormProvider<NonInventoryProduct>
+ name="product"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <InputImage<NonInventoryProduct>
+ name="image"
+ label={i18n.str`Image`}
+ tooltip={i18n.str`photo of the product`}
+ />
+ <Input<NonInventoryProduct>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`full product description`}
+ />
+ <Input<NonInventoryProduct>
+ name="unit"
+ label={i18n.str`Unit`}
+ tooltip={i18n.str`name of the product unit`}
+ />
+ <InputCurrency<NonInventoryProduct>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`amount in the current currency`}
+ />
+
+ <InputNumber<NonInventoryProduct>
+ name="quantity"
+ label={i18n.str`Quantity`}
+ tooltip={i18n.str`how many products will be added`}
+ />
+
+ <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx
new file mode 100644
index 000000000..e91e8c876
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx
@@ -0,0 +1,178 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { useBackendContext } from "../../context/backend.js";
+import { MerchantBackend } from "../../declaration.js";
+import {
+ ProductCreateSchema as createSchema,
+ ProductUpdateSchema as updateSchema,
+} from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputCurrency } from "../form/InputCurrency.js";
+import { InputImage } from "../form/InputImage.js";
+import { InputNumber } from "../form/InputNumber.js";
+import { InputStock, Stock } from "../form/InputStock.js";
+import { InputTaxes } from "../form/InputTaxes.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+ alreadyExist?: boolean;
+}
+
+export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({
+ address: {},
+ description_i18n: {},
+ taxes: [],
+ next_restock: { t_s: "never" },
+ price: ":0",
+ ...initial,
+ stock:
+ !initial || initial.total_stock === -1
+ ? undefined
+ : {
+ current: initial.total_stock || 0,
+ lost: initial.total_lost || 0,
+ sold: initial.total_sold || 0,
+ address: initial.address,
+ nextRestock: initial.next_restock,
+ },
+ });
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ (alreadyExist ? updateSchema : createSchema).validateSync(value, {
+ abortEarly: false,
+ });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): Entity | undefined => {
+ const stock: Stock = (value as any).stock;
+
+ if (!stock) {
+ value.total_stock = -1;
+ } else {
+ value.total_stock = stock.current;
+ value.total_lost = stock.lost;
+ value.next_restock =
+ stock.nextRestock instanceof Date
+ ? { t_s: stock.nextRestock.getTime() / 1000 }
+ : stock.nextRestock;
+ value.address = stock.address;
+ }
+ delete (value as any).stock;
+
+ if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
+ delete value.minimum_age;
+ }
+
+ return value as MerchantBackend.Products.ProductDetail & {
+ product_id: string;
+ };
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { url: backendURL } = useBackendContext()
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <FormProvider<Entity>
+ name="product"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {alreadyExist ? undefined : (
+ <InputWithAddon<Entity>
+ name="product_id"
+ addonBefore={`${backendURL}/product/`}
+ label={i18n.str`ID`}
+ tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
+ />
+ )}
+ <InputImage<Entity>
+ name="image"
+ label={i18n.str`Image`}
+ tooltip={i18n.str`illustration of the product for customers`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`product description for customers`}
+ />
+ <InputNumber<Entity>
+ name="minimum_age"
+ label={i18n.str`Age restriction`}
+ tooltip={i18n.str`is this product restricted for customer below certain age?`}
+ help={i18n.str`minimum age of the buyer`}
+ />
+ <Input<Entity>
+ name="unit"
+ label={i18n.str`Unit name`}
+ tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ help={i18n.str`exajmple: kg, items or liters`}
+ />
+ <InputCurrency<Entity>
+ name="price"
+ label={i18n.str`Price per unit`}
+ tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
+ />
+ <InputStock
+ name="stock"
+ label={i18n.str`Stock`}
+ alreadyExist={alreadyExist}
+ tooltip={i18n.str`inventory for products with finite supply (for internal use only)`}
+ />
+ <InputTaxes<Entity>
+ name="taxes"
+ label={i18n.str`Taxes`}
+ tooltip={i18n.str`taxes included in the product price, exposed to customers`}
+ />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx
new file mode 100644
index 000000000..25751dd96
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx
@@ -0,0 +1,106 @@
+/*
+ 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/>
+ */
+import { Amounts } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import emptyImage from "../../assets/empty.png";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { MerchantBackend } from "../../declaration.js";
+
+interface Props {
+ list: MerchantBackend.Product[];
+ actions?: {
+ name: string;
+ tooltip: string;
+ handler: (d: MerchantBackend.Product, index: number) => void;
+ }[];
+}
+export function ProductList({ list, actions = [] }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>quantity</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>unit price</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>total price</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((entry, index) => {
+ const unitPrice = !entry.price ? "0" : entry.price;
+ const totalPrice = !entry.price
+ ? "0"
+ : Amounts.stringify(
+ Amounts.mult(
+ Amounts.parseOrThrow(entry.price),
+ entry.quantity,
+ ).amount,
+ );
+
+ return (
+ <tr key={index}>
+ <td>
+ <img
+ style={{ height: 32, width: 32 }}
+ src={entry.image ? entry.image : emptyImage}
+ />
+ </td>
+ <td>{entry.description}</td>
+ <td>
+ {entry.quantity === 0
+ ? "--"
+ : `${entry.quantity} ${entry.unit}`}
+ </td>
+ <td>{unitPrice}</td>
+ <td>{totalPrice}</td>
+ <td class="is-actions-cell right-sticky">
+ {actions.map((a, i) => {
+ return (
+ <div key={i} class="buttons is-right">
+ <button
+ class="button is-small is-danger has-tooltip-left"
+ data-tooltip={a.tooltip}
+ type="button"
+ onClick={() => a.handler(entry, index)}
+ >
+ {a.name}
+ </button>
+ </div>
+ );
+ })}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/context/backend.test.ts b/packages/auditor-backoffice-ui/src/context/backend.test.ts
new file mode 100644
index 000000000..359859819
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/backend.test.ts
@@ -0,0 +1,163 @@
+/*
+ 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)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ComponentChildren, h, VNode } from "preact";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useAdminAPI,
+ useInstanceAPI,
+ useManagementAPI,
+} from "../hooks/instance.js";
+import { expect } from "chai";
+import { ApiMockEnvironment } from "../hooks/testing.js";
+import {
+ API_CREATE_INSTANCE,
+ API_NEW_LOGIN,
+ API_UPDATE_CURRENT_INSTANCE_AUTH,
+ API_UPDATE_INSTANCE_AUTH_BY_ID,
+} from "../hooks/urls.js";
+
+interface TestingContextProps {
+ children?: ComponentChildren;
+}
+
+describe("backend context api ", () => {
+ it("should use new token after updating the instance token in the settings as user", async () => {
+ const env = new ApiMockEnvironment();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const instance = useInstanceAPI();
+ const management = useManagementAPI("default");
+ const admin = useAdminAPI();
+
+ return { instance, management, admin };
+ },
+ {},
+ [
+ ({ instance, management, admin }) => {
+ env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
+ request: {
+ method: "token",
+ token: "another_token",
+ },
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "another_token",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+
+ });
+
+ management.setNewAccessToken(undefined,"another_token" as AccessToken);
+ },
+ ({ instance, management, admin }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ // auth: "another_token",
+ request: {
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+
+ admin.createInstance({
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should use new token after updating the instance token in the settings as admin", async () => {
+ const env = new ApiMockEnvironment();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const instance = useInstanceAPI();
+ const management = useManagementAPI("default");
+ const admin = useAdminAPI();
+
+ return { instance, management, admin };
+ },
+ {},
+ [
+ ({ instance, management, admin }) => {
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "token",
+ token: "another_token",
+ },
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "another_token",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+ });
+ instance.setNewAccessToken(undefined, "another_token" as AccessToken);
+ },
+ ({ instance, management, admin }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ // auth: "another_token",
+ request: {
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+
+ admin.createInstance({
+ id: "new_instance_id",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/context/backend.ts b/packages/auditor-backoffice-ui/src/context/backend.ts
new file mode 100644
index 000000000..2dad11f6b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/backend.ts
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useMemoryStorage } from "@gnu-taler/web-util/browser";
+import { createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
+import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
+
+interface BackendContextType {
+ url: string,
+ alreadyTriedLogin: boolean;
+ token?: LoginToken;
+ updateToken: (token: LoginToken | undefined) => void;
+}
+
+const BackendContext = createContext<BackendContextType>({
+ url: "",
+ alreadyTriedLogin: false,
+ token: undefined,
+ updateToken: () => null,
+});
+
+function useBackendContextState(
+ defaultUrl?: string,
+): BackendContextType {
+ const [url] = useBackendURL(defaultUrl);
+ const [token, updateToken] = useBackendDefaultToken();
+
+ return {
+ url,
+ token,
+ alreadyTriedLogin: token !== undefined,
+ updateToken,
+ };
+}
+
+export const BackendContextProvider = ({
+ children,
+ defaultUrl,
+}: {
+ children: any;
+ defaultUrl?: string;
+}): VNode => {
+ const value = useBackendContextState(defaultUrl);
+
+ return h(BackendContext.Provider, { value, children });
+};
+
+export const useBackendContext = (): BackendContextType =>
+ useContext(BackendContext);
diff --git a/packages/auditor-backoffice-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts
new file mode 100644
index 000000000..def45ea64
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/config.ts
@@ -0,0 +1,32 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { createContext } from "preact";
+import { useContext } from "preact/hooks";
+
+interface Type {
+ currency: string;
+ version: string;
+}
+const Context = createContext<Type>(null!);
+
+export const ConfigContextProvider = Context.Provider;
+export const useConfigContext = (): Type => useContext(Context);
diff --git a/packages/auditor-backoffice-ui/src/context/instance.ts b/packages/auditor-backoffice-ui/src/context/instance.ts
new file mode 100644
index 000000000..5800ade7e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/context/instance.ts
@@ -0,0 +1,36 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { createContext } from "preact";
+import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
+
+interface Type {
+ id: string;
+ token?: LoginToken;
+ admin?: boolean;
+ changeToken: (t?: LoginToken) => void;
+}
+
+const Context = createContext<Type>({} as any);
+
+export const InstanceContextProvider = Context.Provider;
+export const useInstanceContext = (): Type => useContext(Context);
diff --git a/packages/auditor-backoffice-ui/src/custom.d.ts b/packages/auditor-backoffice-ui/src/custom.d.ts
new file mode 100644
index 000000000..34522a2dd
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/custom.d.ts
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+declare module "*.po" {
+ const content: any;
+ export default content;
+}
+declare module "jed" {
+ const x: any;
+ export = x;
+}
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.png" {
+ const content: any;
+ export default content;
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
+
+declare module "*.scss" {
+ const content: Record<string, string>;
+ export default content;
+}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts
new file mode 100644
index 000000000..f0c79268c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/declaration.d.ts
@@ -0,0 +1,1830 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+type HashCode = string;
+type EddsaPublicKey = string;
+type EddsaSignature = string;
+type WireTransferIdentifierRawP = string;
+type RelativeTime = TalerProtocolDuration;
+type ImageDataUrl = string;
+type MerchantUserType = "business" | "individual";
+
+
+export interface WithId {
+ id: string;
+}
+
+interface Timestamp {
+ // Milliseconds since epoch, or the special
+ // value "forever" to represent an event that will
+ // never happen.
+ t_s: number | "never";
+}
+interface TalerProtocolDuration {
+ d_us: number | "forever";
+}
+interface Duration {
+ d_ms: number | "forever";
+}
+
+interface WithId {
+ id: string;
+}
+
+type Amount = string;
+type UUID = string;
+type Integer = number;
+
+interface WireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: string;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignature;
+}
+
+type AccountRestriction = RegexAccountRestriction | DenyAllAccountRestriction;
+
+// Account restriction that disables this type of
+// account for the indicated operation categorically.
+interface DenyAllAccountRestriction {
+ type: "deny";
+}
+
+// Accounts interacting with this type of account
+// restriction must have a payto://-URI matching
+// the given regex.
+interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: { [lang_tag: string]: string };
+}
+interface LoginToken {
+ token: string,
+ expiration: Timestamp,
+}
+// token used to get loginToken
+// must forget after used
+declare const __ac_token: unique symbol;
+type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+export namespace ExchangeBackend {
+ interface WireResponse {
+ // Master public key of the exchange, must match the key returned in /keys.
+ master_public_key: EddsaPublicKey;
+
+ // Array of wire accounts operated by the exchange for
+ // incoming wire transfers.
+ accounts: WireAccount[];
+
+ // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank")
+ // to wire fees.
+ fees: { method: AggregateTransferFee };
+ }
+ interface AggregateTransferFee {
+ // Per transfer wire transfer fee.
+ wire_fee: Amount;
+
+ // Per transfer closing fee.
+ closing_fee: Amount;
+
+ // What date (inclusive) does this fee go into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fee stop going into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ end_date: Timestamp;
+
+ // Signature of TALER_MasterWireFeePS with
+ // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
+ sig: EddsaSignature;
+ }
+}
+export namespace AuditorBackend {
+ interface ErrorDetail {
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ code: number;
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ hint?: string;
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ detail?: string;
+
+ // Name of the parameter that was bogus (if applicable).
+ parameter?: string;
+
+ // Path to the argument that was bogus (if applicable).
+ path?: string;
+
+ // Offset of the argument that was bogus (if applicable).
+ offset?: string;
+
+ // Index of the argument that was bogus (if applicable).
+ index?: string;
+
+ // Name of the object that was bogus (if applicable).
+ object?: string;
+
+ // Name of the currency than was problematic (if applicable).
+ currency?: string;
+
+ // Expected type (if applicable).
+ type_expected?: string;
+
+ // Type that was provided instead (if applicable).
+ type_actual?: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+ namespace DepositConfirmation {
+ // POST /deposit-confirmation
+ interface ProductAddDetail {
+ // product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ // PATCH /private/products/$PRODUCT_ID
+ interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // GET /deposit-confirmation
+ interface DepositConfirmationList {
+ depositConfirmations: DepositConfirmation [];
+ }
+ interface DepositConfirmation {
+ serial_id: string;
+ timestamp: string;
+ refund_deadline: string;
+ wire_deadline: string;
+ amount_without_fee: string;
+ }
+
+ // GET /private/products/$PRODUCT_ID
+ interface DepositConfirmationDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ }
+
+}
+export namespace MerchantBackend {
+ interface ErrorDetail {
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ code: number;
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ hint?: string;
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ detail?: string;
+
+ // Name of the parameter that was bogus (if applicable).
+ parameter?: string;
+
+ // Path to the argument that was bogus (if applicable).
+ path?: string;
+
+ // Offset of the argument that was bogus (if applicable).
+ offset?: string;
+
+ // Index of the argument that was bogus (if applicable).
+ index?: string;
+
+ // Name of the object that was bogus (if applicable).
+ object?: string;
+
+ // Name of the currency than was problematic (if applicable).
+ currency?: string;
+
+ // Expected type (if applicable).
+ type_expected?: string;
+
+ // Type that was provided instead (if applicable).
+ type_actual?: string;
+ }
+
+ // Delivery location, loosely modeled as a subset of
+ // ISO20022's PostalAddress25.
+ interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: Amount;
+ }
+
+ interface Auditor {
+ // official name
+ name: string;
+
+ // Auditor's public key
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor
+ url: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+
+ interface Product {
+ // merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity: Integer;
+
+ // The unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for this product. Can be empty.
+ taxes: Tax[];
+
+ // time indicating when this product should be delivered
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ interface Merchant {
+ // label for a location with the business address of the merchant
+ address: Location;
+
+ // the merchant's legal name of business
+ name: string;
+
+ // label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction: Location;
+ }
+
+ interface VersionResponse {
+ // libtool-style representation of the Merchant protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the protocol.
+ name: "taler-merchant";
+
+ // Currency supported by this backend.
+ currency: string;
+ }
+ interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+ }
+ namespace Instances {
+ //POST /private/instances/$INSTANCE/auth
+ interface InstanceAuthConfigurationMessage {
+ // Type of authentication.
+ // "external": The mechant backend does not do
+ // any authentication checks. Instead an API
+ // gateway must do the authentication.
+ // "token": The merchant checks an auth token.
+ // See "token" for details.
+ method: "external" | "token";
+
+ // For method "external", this field is mandatory.
+ // The token MUST begin with the string "secret-token:".
+ // After the auth token has been set (with method "token"),
+ // the value must be provided in a "Authorization: Bearer $token"
+ // header.
+ token?: string;
+ }
+ //POST /private/instances
+ interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ id: string;
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // "Authentication" header required to authorize management access the instance.
+ // Optional, if not given authentication will be disabled for
+ // this instance (hopefully authentication checks are still
+ // done by some reverse proxy).
+ auth: InstanceAuthConfigurationMessage;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // PATCH /private/instances/$INSTANCE
+ interface InstanceReconfigurationMessage {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // GET /private/instances
+ interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance)
+ instances: Instance[];
+ }
+
+ interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Merchant instance this response is about ($INSTANCE)
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // List of the payment targets supported by this instance. Clients can
+ // specify the desired payment target in /order requests. Note that
+ // front-ends do not have to support wallets selecting payment targets.
+ payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
+ }
+
+ //GET /private/instances/$INSTANCE
+ interface QueryInstancesResponse {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+ // DELETE /private/instances/$INSTANCE
+ interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+ }
+ interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ token: string;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+ }
+ }
+
+ namespace KYC {
+ //GET /private/instances/$INSTANCE/kyc
+ interface AccountKycRedirects {
+ // Array of pending KYCs.
+ pending_kycs: MerchantAccountKycRedirect[];
+
+ // Array of exchanges with no reply.
+ timeout_kycs: ExchangeKycTimeout[];
+ }
+ interface MerchantAccountKycRedirect {
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (as returned
+ // by the exchange's /kyc-check/ endpoint).
+ // Optional, missing if the account is blocked
+ // due to AML and not due to KYC.
+ kyc_url?: string;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // AML status of the account.
+ aml_status: number;
+
+ // Our bank wire account this is about.
+ payto_uri: string;
+ }
+ interface ExchangeKycTimeout {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // 0 if there was no response at all.
+ exchange_http_status: number;
+ }
+
+ }
+
+ namespace BankAccounts {
+
+ interface AccountAddDetails {
+
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+
+ }
+
+ type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+
+ interface NoFacadeCredentials {
+ type: "none";
+ }
+
+ interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+
+ interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ interface AccountPatchDetails {
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+
+ interface AccountsSummaryResponse {
+
+ // List of accounts that are known for the instance.
+ accounts: BankAccountEntry[];
+ }
+
+ interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // Hash over the wire details (including over the salt)
+ h_wire: HashCode;
+
+ // salt used to compute h_wire
+ salt: HashCode;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ credit_facade_credentials?: FacadeCredentials;
+
+ // true if this account is active,
+ // false if it is historic.
+ active: boolean;
+ }
+
+ }
+
+ namespace Products {
+ // POST /private/products
+ interface ProductAddDetail {
+ // product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ // PATCH /private/products/$PRODUCT_ID
+ interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // GET /private/products
+ interface InventorySummaryResponse {
+ // List of products that are present in the inventory
+ products: InventoryEntry[];
+ }
+ interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ }
+
+ // GET /private/products/$PRODUCT_ID
+ interface ProductDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // POST /private/products/$PRODUCT_ID/lock
+ interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // It is suggested that clients use a timeflake for this,
+ // see https://github.com/anthonynsimon/timeflake
+ lock_uuid: UUID;
+
+ // How long does the frontend intend to hold the lock
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+ }
+
+ // DELETE /private/products/$PRODUCT_ID
+ }
+
+ namespace Orders {
+ type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+ interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: Amount;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_ec: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_hc: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: Amount;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+ }
+ interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // when was the order created
+ creation_time: Timestamp;
+
+ // Order summary text.
+ summary: string;
+
+ // Total amount of the order (to be paid by the customer).
+ total_amount: Amount;
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: Amount;
+ }
+ interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: Amount;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_ec: number;
+
+ // HTTP status code received from the exchange.
+ exchange_hc: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
+ interface OrderHistory {
+ // timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ interface OrderHistoryEntry {
+ // order ID of the transaction related to this entry.
+ order_id: string;
+
+ // row ID of the order in the database
+ row_id: number;
+
+ // when the order was created
+ timestamp: Timestamp;
+
+ // the amount of money the order is for
+ amount: Amount;
+
+ // the summary of the order
+ summary: string;
+
+ // whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // whether the order has been paid or not
+ paid: boolean;
+ }
+
+ interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all
+ order: Order;
+
+ // if set, the backend will then set the refund deadline to the current
+ // time plus the specified delay. If it's not set, refunds will not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // specifies the payment target preferred by the client. Can be used
+ // to select among the various (active) wire methods supported by the instance.
+ payment_target?: string;
+
+ // specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // manage_inventory is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping card was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: UUID[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ interface MinimalOrderDetail {
+ // Amount to be paid by the customer
+ amount: Amount;
+
+ // Short summary of the order
+ summary: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ fulfillment_url?: string;
+ }
+
+ interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!)
+ product_id: string;
+
+ // How many units of the product are requested
+ quantity: Integer;
+ }
+ interface PostOrderResponse {
+ // Order ID of the response that was just created
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ interface OutOfStockResponse {
+ // Product ID of an out-of-stock item
+ product_id: string;
+
+ // Requested quantity
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quanitity)
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+ interface RefundRequest {
+ // Amount to be refunded
+ refund: Amount;
+
+ // Human-readable refund justification
+ reason: string;
+ }
+ interface MerchantRefundResponse {
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+ }
+
+ namespace Rewards {
+ // GET /private/reserves
+ interface RewardReserveStatus {
+ // Array of all known reserves (possibly empty!)
+ reserves: ReserveStatusEntry[];
+ }
+ interface ReserveStatusEntry {
+ // Public key of the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Timestamp when it was established
+ creation_time: Timestamp;
+
+ // Timestamp when it expires
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Is this reserve active (false if it was deleted but not purged)
+ active: boolean;
+ }
+
+ interface ReserveCreateRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: Amount;
+
+ // Exchange the merchant intends to use for reward
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+ }
+ interface ReserveCreateConfirmation {
+ // Public key identifying the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: WireAccount[];
+ }
+ interface RewardCreateRequest {
+ // Amount that the customer should be reward
+ amount: Amount;
+
+ // Justification for giving the reward
+ justification: string;
+
+ // URL that the user should be directed to after rewarding,
+ // will be included in the reward_token.
+ next_url: string;
+ }
+ interface RewardCreateConfirmation {
+ // Unique reward identifier for the reward that was created.
+ reward_id: HashCode;
+
+ // taler://reward URI for the reward
+ taler_reward_uri: string;
+
+ // URL that will directly trigger processing
+ // the reward when the browser is redirected to it
+ reward_status_url: string;
+
+ // when does the reward expire
+ reward_expiration: Timestamp;
+ }
+
+ interface ReserveDetail {
+ // Timestamp when it was established.
+ creation_time: Timestamp;
+
+ // Timestamp when it expires.
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call.
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Array of all rewards created by this reserves (possibly empty!).
+ // Only present if asked for explicitly.
+ rewards?: RewardStatusEntry[];
+
+ // Is this reserve active (false if it was deleted but not purged)?
+ active: boolean;
+
+ // Array of wire accounts of the exchange that could
+ // be used to fill the reserve, can be NULL
+ // if the reserve is inactive or was already filled
+ accounts?: WireAccount[];
+
+ // URL of the exchange hosting the reserve,
+ // NULL if the reserve is inactive
+ exchange_url: string;
+ }
+
+ interface RewardStatusEntry {
+ // Unique identifier for the reward.
+ reward_id: HashCode;
+
+ // Total amount of the reward that can be withdrawn.
+ total_amount: Amount;
+
+ // Human-readable reason for why the reward was granted.
+ reason: string;
+ }
+
+ interface RewardDetails {
+ // Amount that we authorized for this reward.
+ total_authorized: Amount;
+
+ // Amount that was picked up by the user already.
+ total_picked_up: Amount;
+
+ // Human-readable reason given when authorizing the reward.
+ reason: string;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ expiration: Timestamp;
+
+ // Reserve public key from which the reward is funded.
+ reserve_pub: EddsaPublicKey;
+
+ // Array showing the pickup operations of the wallet (possibly empty!).
+ // Only present if asked for explicitly.
+ pickups?: PickupDetail[];
+ }
+ interface PickupDetail {
+ // Unique identifier for the pickup operation.
+ pickup_id: HashCode;
+
+ // Number of planchets involved.
+ num_planchets: Integer;
+
+ // Total amount requested for this pickup_id.
+ requested_amount: Amount;
+ }
+ }
+
+ namespace Transfers {
+ interface TransferList {
+ // list of all the transfers that fit the filter that we know
+ transfers: TransferDetails[];
+ }
+ interface TransferDetails {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: string;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filgering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ interface TransferInformation {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: WireTransferIdentifierRawP;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+ }
+ }
+
+ namespace OTP {
+ interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string | undefined;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+
+ }
+ namespace Template {
+ interface TemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+ interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: Amount;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: Integer;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: RelativeTime;
+ }
+ interface TemplatePatchDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+ }
+
+ interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+ }
+
+ interface TemplateDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface UsingTemplateDetails {
+ // Subject of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: Amount;
+ }
+
+ interface UsingTemplateResponse {
+ // After enter the request. The user will be pay with a taler URL.
+ order_id: string;
+ token: string;
+ }
+ }
+
+ namespace Webhooks {
+ type MerchantWebhookType = "pay" | "refund";
+ interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: MerchantWebhookType;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookPatchDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookSummaryResponse {
+ // List of webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+ }
+ interface WebhookEntry {
+ // Webhook identifier, as found in the webhook.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+ }
+ interface WebhookDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ }
+
+ interface ContractTerms {
+ // Human-readable description of the whole purchase
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries
+ summary_i18n?: { [lang_tag: string]: string };
+
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
+
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
+ amount: Amount;
+
+ // The URL for this purchase. Every time is is visited, the merchant
+ // will send back to the customer the same proposal. Clearly, this URL
+ // can be bookmarked and shared by users.
+ fulfillment_url?: string;
+
+ // Maximum total deposit fee accepted by the merchant for this contract
+ max_fee: Amount;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated
+ timestamp: TalerProtocolTimestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: TalerProtocolTimestamp;
+
+ // After this deadline, the merchant won't accept payments for the contact
+ pay_deadline: TalerProtocolTimestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend Note that this can be an ephemeral key.
+ merchant_pub: EddsaPublicKey;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // More info about the merchant, see below
+ merchant: Merchant;
+
+ // The hash of the merchant instance's wire details.
+ h_wire: HashCode;
+
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
+ wire_method: string;
+
+ // Any exchanges audited by these auditors are accepted by the merchant.
+ auditors: Auditor[];
+
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
+ exchanges: Exchange[];
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
+ auto_refund?: RelativeTime;
+
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ extra?: any;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/async.ts b/packages/auditor-backoffice-ui/src/hooks/async.ts
new file mode 100644
index 000000000..f22badc88
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/async.ts
@@ -0,0 +1,77 @@
+/*
+ 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)
+ */
+import { useState } from "preact/hooks";
+
+export interface Options {
+ slowTolerance: number;
+}
+
+export interface AsyncOperationApi<T> {
+ request: (...a: any) => void;
+ cancel: () => void;
+ data: T | undefined;
+ isSlow: boolean;
+ isLoading: boolean;
+ error: string | undefined;
+}
+
+export function useAsync<T>(
+ fn?: (...args: any) => Promise<T>,
+ { slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
+): AsyncOperationApi<T> {
+ const [data, setData] = useState<T | undefined>(undefined);
+ const [isLoading, setLoading] = useState<boolean>(false);
+ const [error, setError] = useState<any>(undefined);
+ const [isSlow, setSlow] = useState(false);
+
+ const request = async (...args: any) => {
+ if (!fn) return;
+ setLoading(true);
+
+ const handler = setTimeout(() => {
+ setSlow(true);
+ }, tooLong);
+
+ try {
+ const result = await fn(...args);
+ setData(result);
+ } catch (error) {
+ setError(error);
+ }
+ setLoading(false);
+ setSlow(false);
+ clearTimeout(handler);
+ };
+
+ function cancel(): void {
+ setLoading(false);
+ setSlow(false);
+ }
+
+ return {
+ request,
+ cancel,
+ data,
+ isSlow,
+ isLoading,
+ error,
+ };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts
new file mode 100644
index 000000000..8d99546a8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts
@@ -0,0 +1,477 @@
+/*
+ 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)
+ */
+
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+ RequestOptions,
+ useApiContext,
+} from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useSWRConfig } from "swr";
+import { useBackendContext } from "../context/backend.js";
+import { useInstanceContext } from "../context/instance.js";
+import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
+
+
+export function useMatchMutate(): (
+ re?: RegExp,
+ value?: unknown,
+) => Promise<any> {
+ const { cache, mutate } = useSWRConfig();
+
+ if (!(cache instanceof Map)) {
+ throw new Error(
+ "matchMutate requires the cache provider to be a Map instance",
+ );
+ }
+
+ return function matchRegexMutate(re?: RegExp) {
+ return mutate((key) => {
+ // evict if no key or regex === all
+ if (!key || !re) return true
+ // match string
+ if (typeof key === 'string' && re.test(key)) return true
+ // record or object have the path at [0]
+ if (typeof key === 'object' && re.test(key[0])) return true
+ //key didn't match regex
+ return false
+ }, undefined, {
+ revalidate: true,
+ });
+ };
+}
+
+export function useBackendInstancesTestForAdmin(): HttpResponse<
+ MerchantBackend.Instances.InstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ type Type = MerchantBackend.Instances.InstancesResponse;
+
+ const [result, setResult] = useState<
+ HttpResponse<Type, MerchantBackend.ErrorDetail>
+ >({ loading: true });
+
+ useEffect(() => {
+ request<Type>(`/management/instances`)
+ .then((data) => setResult(data))
+ .catch((error: RequestError<MerchantBackend.ErrorDetail>) =>
+ setResult(error.cause),
+ );
+ }, [request]);
+
+ return result;
+}
+
+const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
+const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+
+export function useBackendConfig(): HttpResponse<
+ MerchantBackend.VersionResponse | undefined,
+ RequestError<MerchantBackend.ErrorDetail>
+> {
+ const { request } = useBackendBaseRequest();
+
+ type Type = MerchantBackend.VersionResponse;
+ type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
+ const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
+
+ useEffect(() => {
+ if (result.timer) {
+ clearTimeout(result.timer)
+ }
+ function tryConfig(): void {
+ request<Type>(`/config`)
+ .then((data) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_OK)
+ setResult({ data, timer })
+ })
+ .catch((error) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_FAIL)
+ const data = error.cause
+ setResult({ data, timer })
+ });
+ }
+ tryConfig()
+ }, [request]);
+
+ return result.data;
+}
+
+interface useBackendInstanceRequestType {
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions,
+ ) => Promise<HttpResponseOk<T>>;
+ fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
+ orderFetcher: <T>(
+ params: [endpoint: string,
+ paid?: YesOrNo,
+ refunded?: YesOrNo,
+ wired?: YesOrNo,
+ searchDate?: Date,
+ delta?: number,]
+ ) => Promise<HttpResponseOk<T>>;
+ transferFetcher: <T>(
+ params: [endpoint: string,
+ payto_uri?: string,
+ verified?: string,
+ position?: string,
+ delta?: number,]
+ ) => Promise<HttpResponseOk<T>>;
+ templateFetcher: <T>(
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
+ ) => Promise<HttpResponseOk<T>>;
+ webhookFetcher: <T>(
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
+ ) => Promise<HttpResponseOk<T>>;
+}
+interface useBackendBaseRequestType {
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions,
+ ) => Promise<HttpResponseOk<T>>;
+}
+
+type YesOrNo = "yes" | "no";
+type LoginResult = {
+ valid: true;
+ token: string;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
+
+export function useCredentialsChecker() {
+ const { request } = useApiContext();
+ //check against instance details endpoint
+ //while merchant backend doesn't have a login endpoint
+ async function requestNewLoginToken(
+ baseUrl: string,
+ token: AccessToken,
+ ): Promise<LoginResult> {
+ const data: MerchantBackend.Instances.LoginTokenRequest = {
+ scope: "write",
+ duration: {
+ d_us: "forever"
+ },
+ refreshable: true,
+ }
+ try {
+ const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
+ method: "POST",
+ token,
+ data
+ });
+ return { valid: true, token: response.data.token, expiration: response.data.expiration };
+ } catch (error) {
+ if (error instanceof RequestError) {
+ return { valid: false, cause: error.cause };
+ }
+
+ return {
+ valid: false, cause: {
+ type: ErrorType.UNEXPECTED,
+ loading: false,
+ info: {
+ hasToken: true,
+ status: 0,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ exception: error,
+ message: (error instanceof Error ? error.message : "unpexepected error")
+ }
+ };
+ }
+ };
+
+ async function refreshLoginToken(
+ baseUrl: string,
+ token: LoginToken
+ ): Promise<LoginResult> {
+
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ return {
+ valid: false, cause: {
+ type: ErrorType.CLIENT,
+ status: HttpStatusCode.Unauthorized,
+ message: "login token expired, login again.",
+ info: {
+ hasToken: true,
+ status: 401,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ payload: {}
+ },
+ }
+ }
+
+ return requestNewLoginToken(baseUrl, token.token as AccessToken)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
+}
+
+/**
+ *
+ * @param root the request is intended to the base URL and no the instance URL
+ * @returns request handler to
+ */
+export function useBackendBaseRequest(): useBackendBaseRequestType {
+ const { url: backend, token: loginToken } = useBackendContext();
+ const { request: requestHandler } = useApiContext();
+ const token = loginToken?.token;
+
+ const request = useCallback(
+ function requestImpl<T>(
+ endpoint: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
+ return res
+ }).catch(err => {
+ throw err
+ });
+ },
+ [backend, token],
+ );
+
+ return { request };
+}
+
+export function useBackendInstanceRequest(): useBackendInstanceRequestType {
+ const { url: rootBackendUrl, token: rootToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+ const { request: requestHandler } = useApiContext();
+
+ const { baseUrl, token: loginToken } = !admin
+ ? { baseUrl: rootBackendUrl, token: rootToken }
+ : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
+
+ const token = loginToken?.token;
+
+ const request = useCallback(
+ function requestImpl<T>(
+ endpoint: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, { token, ...options });
+ },
+ [baseUrl, token],
+ );
+
+ const multiFetcher = useCallback(
+ function multiFetcherImpl<T>(
+ args: [endpoints: string[]],
+ ): Promise<HttpResponseOk<T>[]> {
+ const [endpoints] = args
+ return Promise.all(
+ endpoints.map((endpoint) =>
+ requestHandler<T>(baseUrl, endpoint, { token }),
+ ),
+ );
+ },
+ [baseUrl, token],
+ );
+
+ const fetcher = useCallback(
+ function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, { token });
+ },
+ [baseUrl, token],
+ );
+
+ const orderFetcher = useCallback(
+ function orderFetcherImpl<T>(
+ args: [endpoint: string,
+ paid?: YesOrNo,
+ refunded?: YesOrNo,
+ wired?: YesOrNo,
+ searchDate?: Date,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, paid, refunded, wired, searchDate, delta] = args
+ const date_s =
+ delta && delta < 0 && searchDate
+ ? Math.floor(searchDate.getTime() / 1000) + 1
+ : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined;
+ const params: any = {};
+ if (paid !== undefined) params.paid = paid;
+ if (delta !== undefined) params.delta = delta;
+ if (refunded !== undefined) params.refunded = refunded;
+ if (wired !== undefined) params.wired = wired;
+ if (date_s !== undefined) params.date_s = date_s;
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { orders: [] } as T,
+ })
+ }
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const reserveDetailFetcher = useCallback(
+ function reserveDetailFetcherImpl<T>(
+ endpoint: string,
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {
+ params: {
+ rewards: "yes",
+ },
+ token,
+ });
+ },
+ [baseUrl, token],
+ );
+
+ const rewardsDetailFetcher = useCallback(
+ function rewardsDetailFetcherImpl<T>(
+ endpoint: string,
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {
+ params: {
+ pickups: "yes",
+ },
+ token,
+ });
+ },
+ [baseUrl, token],
+ );
+
+ const transferFetcher = useCallback(
+ function transferFetcherImpl<T>(
+ args: [endpoint: string,
+ payto_uri?: string,
+ verified?: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, payto_uri, verified, position, delta] = args
+ const params: any = {};
+ if (payto_uri !== undefined) params.payto_uri = payto_uri;
+ if (verified !== undefined) params.verified = verified;
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { transfers: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const templateFetcher = useCallback(
+ function templateFetcherImpl<T>(
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
+ const params: any = {};
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { templates: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ const webhookFetcher = useCallback(
+ function webhookFetcherImpl<T>(
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
+ const params: any = {};
+ if (delta === 0) {
+ //in this case we can already assume the response
+ //and avoid network
+ return Promise.resolve({
+ ok: true,
+ data: { webhooks: [] } as T,
+ })
+ }
+ if (delta !== undefined) {
+ params.limit = delta;
+ }
+ if (position !== undefined) params.offset = position;
+
+ return requestHandler<T>(baseUrl, endpoint, { params, token });
+ },
+ [baseUrl, token],
+ );
+
+ return {
+ request,
+ fetcher,
+ multiFetcher,
+ orderFetcher,
+ reserveDetailFetcher,
+ rewardsDetailFetcher,
+ transferFetcher,
+ templateFetcher,
+ webhookFetcher,
+ };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/bank.ts b/packages/auditor-backoffice-ui/src/hooks/bank.ts
new file mode 100644
index 000000000..03b064646
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/bank.ts
@@ -0,0 +1,217 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = {
+// "hwire1": {
+// h_wire: "hwire1",
+// payto_uri: "payto://fake/iban/123",
+// salt: "qwe",
+// },
+// "hwire2": {
+// h_wire: "hwire2",
+// payto_uri: "payto://fake/iban/123",
+// salt: "qwe2",
+// },
+// }
+
+export function useBankAccountAPI(): BankAccountAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createBankAccount = async (
+ data: MerchantBackend.BankAccounts.AccountAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_ACCOUNTS[data.h_wire] = data
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ const updateBankAccount = async (
+ h_wire: string,
+ data: MerchantBackend.BankAccounts.AccountPatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials
+ // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts/${h_wire}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ const deleteBankAccount = async (
+ h_wire: string,
+ ): Promise<HttpResponseOk<void>> => {
+ // delete MOCKED_ACCOUNTS[h_wire]
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/accounts/${h_wire}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/accounts.*/);
+ return res;
+ };
+
+ return {
+ createBankAccount,
+ updateBankAccount,
+ deleteBankAccount,
+ };
+}
+
+export interface BankAccountAPI {
+ createBankAccount: (
+ data: MerchantBackend.BankAccounts.AccountAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateBankAccount: (
+ id: string,
+ data: MerchantBackend.BankAccounts.AccountPatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceBankAccountFilter {
+}
+
+export function useInstanceBankAccounts(
+ args?: InstanceBankAccountFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.BankAccounts.AccountsSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // loadMore() { },
+ // loadMorePrev() { },
+ // data: {
+ // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({
+ // ...e,
+ // active: true,
+ // }))
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/accounts`], fetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.BankAccounts.AccountsSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData /*, beforeData*/]);
+
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.accounts.length < totalAfter;
+ const isReachingStart = false;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.accounts.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.accounts[afterData.data.accounts.length - 1]
+ .h_wire
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ },
+ };
+
+ const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts;
+ if (loadingAfter /* || loadingBefore */)
+ return { loading: true, data: { accounts } };
+ if (/*beforeData &&*/ afterData) {
+ return { ok: true, data: { accounts }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useBankAccountDetails(
+ h_wire: string,
+): HttpResponse<
+ MerchantBackend.BankAccounts.BankAccountEntry,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // data: {
+ // ...MOCKED_ACCOUNTS[h_wire],
+ // active: true,
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/accounts/${h_wire}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts
new file mode 100644
index 000000000..e4ec9a2f2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts
@@ -0,0 +1,161 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface DepositConfirmationAPI {
+ getDepositConfirmation: (
+ id: string,
+ ) => Promise<void>;
+ createDepositConfirmation: (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ) => Promise<void>;
+ updateDepositConfirmation: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ deleteDepositConfirmation: (id: string) => Promise<void>;
+}
+
+export function useDepositConfirmationAPI(): DepositConfirmationAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+
+ const { request } = useBackendInstanceRequest();
+
+ const createDepositConfirmation = async (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ): Promise<void> => {
+ const res = await request(`/private/products`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const updateDepositConfirmation = async (
+ productId: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ): Promise<void> => {
+ const r = await request(`/private/products/${productId}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const deleteDepositConfirmation = async (productId: string): Promise<void> => {
+ await request(`/private/products/${productId}`, {
+ method: "DELETE",
+ });
+ await mutate([`/private/products`]);
+ };
+
+ const getDepositConfirmation = async (
+ serialId: string,
+ ): Promise<void> => {
+ await request(`/deposit-confirmation/${serialId}`, {
+ method: "GET",
+ });
+
+ return
+ };
+
+ return {createDepositConfirmation, updateDepositConfirmation, deleteDepositConfirmation, getDepositConfirmation};
+}
+
+export function useDepositConfirmation(): HttpResponse<
+ (AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId)[],
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher, multiFetcher } = useBackendInstanceRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationList>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ const paths = (list?.data.depositConfirmations || []).map(
+ (p) => `/deposit-confirmation/${p.serial_id}`,
+ );
+ const { data: depositConfirmations, error: depositConfirmationError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >([paths], multiFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+ if (depositConfirmationError) return depositConfirmationError.cause;
+
+ if (depositConfirmations) {
+ const dataWithId = depositConfirmations.map((d) => {
+ //take the id from the queried url
+ return {
+ ...d.data,
+ id: d.info?.url.replace(/.*\/deposit-confirmation\//, "") || "",
+ };
+ });
+ return { ok: true, data: dataWithId };
+ }
+ return { loading: true };
+}
+
+export function useDepositConfirmationDetails(
+ serialId: string,
+): HttpResponse<
+ AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ AuditorBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`/deposit-confirmation/${serialId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts
new file mode 100644
index 000000000..61afbc94a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/index.ts
@@ -0,0 +1,151 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
+import { ValueOrFunction } from "../utils/types.js";
+import { useMatchMutate } from "./backend.js";
+
+const calculateRootPath = () => {
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
+
+ /**
+ * By default, merchant backend serves the html content
+ * from the /webui root. This should cover most of the
+ * cases and the rootPath will be the merchant backend
+ * URL where the instances are
+ */
+ return rootPath.replace("/webui/", "");
+};
+
+const loginTokenCodec = buildCodecForObject<LoginToken>()
+ .property("token", codecForString())
+ .property("expiration", codecForTimestamp)
+ .build("loginToken")
+const TOKENS_KEY = buildStorageKey("auditor-token", codecForMap(loginTokenCodec));
+
+
+export function useBackendURL(
+ url?: string,
+): [string, StateUpdater<string>] {
+ const [value, setter] = useSimpleLocalStorage(
+ "auditor-base-url",
+ url || calculateRootPath(),
+ );
+
+ const checkedSetter = (v: ValueOrFunction<string>) => {
+ return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
+ };
+
+ return [value!, checkedSetter];
+}
+
+export function useBackendDefaultToken(
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+
+ const tokenOfDefaultInstance = tokenMap["default"]
+ const clearCache = useMatchMutate()
+ useEffect(() => {
+ clearCache()
+ }, [tokenOfDefaultInstance])
+
+ function updateToken(
+ value: (LoginToken | undefined)
+ ): void {
+ if (value === undefined) {
+ reset()
+ } else {
+ const res = { ...tokenMap, "default": value }
+ setToken(res)
+ }
+ }
+ return [tokenMap["default"], updateToken];
+}
+
+export function useBackendInstanceToken(
+ id: string,
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+ const [defaultToken, defaultSetToken] = useBackendDefaultToken();
+
+ // instance named 'default' use the default token
+ if (id === "default") {
+ return [defaultToken, defaultSetToken];
+ }
+ function updateToken(
+ value: (LoginToken | undefined)
+ ): void {
+ if (value === undefined) {
+ reset()
+ } else {
+ const res = { ...tokenMap, [id]: value }
+ setToken(res)
+ }
+ }
+
+ return [tokenMap[id], updateToken];
+}
+
+export function useLang(initial?: string): [string, StateUpdater<string>] {
+ const browserLang =
+ typeof window !== "undefined"
+ ? navigator.language || (navigator as any).userLanguage
+ : undefined;
+ const defaultLang = (browserLang || initial || "en").substring(0, 2);
+ return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>];
+}
+
+export function useSimpleLocalStorage(
+ key: string,
+ initialValue?: string,
+): [string | undefined, StateUpdater<string | undefined>] {
+ const [storedValue, setStoredValue] = useState<string | undefined>(
+ (): string | undefined => {
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
+ },
+ );
+
+ const setValue = (
+ value?: string | ((val?: string) => string | undefined),
+ ) => {
+ setStoredValue((p) => {
+ const toStore = value instanceof Function ? value(p) : value;
+ if (typeof window !== "undefined") {
+ if (!toStore) {
+ window.localStorage.removeItem(key);
+ } else {
+ window.localStorage.setItem(key, toStore);
+ }
+ }
+ return toStore;
+ });
+ };
+
+ return [storedValue, setValue];
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
new file mode 100644
index 000000000..ee1576764
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
@@ -0,0 +1,741 @@
+/*
+ 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)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useAdminAPI,
+ useBackendInstances,
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagementAPI,
+} from "./instance.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_INSTANCE,
+ API_DELETE_INSTANCE,
+ API_GET_CURRENT_INSTANCE,
+ API_LIST_INSTANCES,
+ API_NEW_LOGIN,
+ API_UPDATE_CURRENT_INSTANCE,
+ API_UPDATE_CURRENT_INSTANCE_AUTH,
+ API_UPDATE_INSTANCE_BY_ID,
+} from "./urls.js";
+
+describe("instance api interaction with details", () => {
+ it("should evict cache when updating an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "other_name",
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ api.updateInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "other_name",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when setting the instance's token", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ },
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "token",
+ token: "secret",
+ } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ });
+ env.addRequestExpectation(API_NEW_LOGIN, {
+ auth: "secret",
+ request: {
+ scope: "write",
+ duration: {
+ "d_us": "forever",
+ },
+ refreshable: true,
+ },
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+ api.setNewAccessToken(undefined, "secret" as AccessToken);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "secret",
+ },
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when clearing the instance's token", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useInstanceAPI();
+ const query = useInstanceDetails();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "token",
+ // token: "not-secret",
+ },
+ });
+ env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+ request: {
+ method: "external",
+ } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ });
+ env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+ response: {
+ name: "instance_name",
+ auth: {
+ method: "external",
+ },
+ } as MerchantBackend.Instances.QueryInstancesResponse,
+ });
+
+ api.clearAccessToken(undefined);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ name: "instance_name",
+ auth: {
+ method: "external",
+ },
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => {
+ // const api = useInstanceAPI();
+ // const query = useInstanceDetails();
+
+ // return { query, api };
+ // },
+ // { wrapper: TestingContext }
+ // );
+
+ // expect(result.current).not.undefined;
+ // if (!result.current) {
+ // return;
+ // }
+ // expect(result.current.query.loading).true;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // expect(result.current?.query.ok).true;
+ // if (!result.current?.query.ok) return;
+
+ // expect(result.current.query.data).equals({
+ // name: 'instance_name',
+ // auth: {
+ // method: 'token',
+ // token: 'not-secret',
+ // }
+ // });
+
+ // act(async () => {
+ // await result.current?.api.clearToken();
+ // });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+ // expect(result.current.query.ok).true;
+
+ // expect(result.current.query.data).equals({
+ // name: 'instance_name',
+ // auth: {
+ // method: 'external',
+ // }
+ // });
+ });
+});
+
+describe("instance admin api interaction with listing", () => {
+ it("should evict cache when creating a new instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ name: "instance_name",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_CREATE_INSTANCE, {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ name: "other_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.createInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ name: "instance_name",
+ },
+ {
+ name: "other_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ {
+ id: "the_id",
+ name: "second_instance",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {});
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.deleteInstance("the_id");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => {
+ // const api = useAdminAPI();
+ // const query = useBackendInstances();
+
+ // return { query, api };
+ // },
+ // { wrapper: TestingContext }
+ // );
+
+ // expect(result.current).not.undefined;
+ // if (!result.current) {
+ // return;
+ // }
+ // expect(result.current.query.loading).true;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+
+ // expect(result.current?.query.ok).true;
+ // if (!result.current?.query.ok) return;
+
+ // expect(result.current.query.data).equals({
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // }, {
+ // id: 'the_id',
+ // name: 'second_instance'
+ // }]
+ // });
+
+ // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
+
+ // act(async () => {
+ // await result.current?.api.deleteInstance('the_id');
+ // });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // env.addRequestExpectation(API_LIST_INSTANCES, {
+ // response: {
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // } as MerchantBackend.Instances.Instance]
+ // },
+ // });
+
+ // expect(result.current.query.loading).false;
+
+ // await waitForNextUpdate({ timeout: 1 });
+
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+
+ // expect(result.current.query.loading).false;
+ // expect(result.current.query.ok).true;
+
+ // expect(result.current.query.data).equals({
+ // instances: [{
+ // id: 'default',
+ // name: 'instance_name'
+ // }]
+ // });
+ });
+
+ it("should evict cache when deleting (purge) an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ {
+ id: "the_id",
+ name: "second_instance",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useAdminAPI();
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ {
+ id: "the_id",
+ name: "second_instance",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {
+ qparam: {
+ purge: "YES",
+ },
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.purgeInstance("the_id");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "default",
+ name: "instance_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("instance management api interaction with listing", () => {
+ it("should evict cache when updating an instance", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "managed",
+ name: "instance_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useManagementAPI("managed");
+ const query = useBackendInstances();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "managed",
+ name: "instance_name",
+ },
+ ],
+ });
+
+ env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), {
+ request: {
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+ });
+ env.addRequestExpectation(API_LIST_INSTANCES, {
+ response: {
+ instances: [
+ {
+ id: "managed",
+ name: "other_name",
+ } as MerchantBackend.Instances.Instance,
+ ],
+ },
+ });
+
+ api.updateInstance({
+ name: "other_name",
+ } as MerchantBackend.Instances.InstanceConfigurationMessage);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ instances: [
+ {
+ id: "managed",
+ name: "other_name",
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.ts b/packages/auditor-backoffice-ui/src/hooks/instance.ts
new file mode 100644
index 000000000..0677191db
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/instance.ts
@@ -0,0 +1,313 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useBackendContext } from "../context/backend.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
+import {
+ useBackendBaseRequest,
+ useBackendInstanceRequest,
+ useCredentialsChecker,
+ useMatchMutate,
+} from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+interface InstanceAPI {
+ updateInstance: (
+ data: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ) => Promise<void>;
+ deleteInstance: () => Promise<void>;
+ clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>;
+ setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>;
+}
+
+export function useAdminAPI(): AdminAPI {
+ const { request } = useBackendBaseRequest();
+ const mutateAll = useMatchMutate();
+
+ const createInstance = async (
+ instance: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ): Promise<void> => {
+ await request(`/management/instances`, {
+ method: "POST",
+ data: instance,
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const deleteInstance = async (id: string): Promise<void> => {
+ await request(`/management/instances/${id}`, {
+ method: "DELETE",
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const purgeInstance = async (id: string): Promise<void> => {
+ await request(`/management/instances/${id}`, {
+ method: "DELETE",
+ params: {
+ purge: "YES",
+ },
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ return { createInstance, deleteInstance, purgeInstance };
+}
+
+export interface AdminAPI {
+ createInstance: (
+ data: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ) => Promise<void>;
+ deleteInstance: (id: string) => Promise<void>;
+ purgeInstance: (id: string) => Promise<void>;
+}
+
+export function useManagementAPI(instanceId: string): InstanceAPI {
+ const mutateAll = useMatchMutate();
+ const { url: backendURL } = useBackendContext()
+ const { updateToken } = useBackendContext();
+ const { request } = useBackendBaseRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
+
+ const updateInstance = async (
+ instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ await request(`/management/instances/${instanceId}`, {
+ method: "PATCH",
+ data: instance,
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`/management/instances/${instanceId}`, {
+ method: "DELETE",
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ await request(`/management/instances/${instanceId}/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "external" },
+ });
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ await request(`/management/instances/${instanceId}/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "token", token: newToken },
+ });
+
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
+ mutateAll(/\/management\/instances/);
+ };
+
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+}
+
+export function useInstanceAPI(): InstanceAPI {
+ const { mutate } = useSWRConfig();
+ const { url: backendURL, updateToken } = useBackendContext()
+
+ const {
+ token: adminToken,
+ } = useBackendContext();
+ const { request } = useBackendInstanceRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
+
+ const updateInstance = async (
+ instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ await request(`/private/`, {
+ method: "PATCH",
+ data: instance,
+ });
+
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ mutate([`/private/`], null);
+ };
+
+ const deleteInstance = async (): Promise<void> => {
+ await request(`/private/`, {
+ method: "DELETE",
+ // token: adminToken,
+ });
+
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ mutate([`/private/`], null);
+ };
+
+ const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ await request(`/private/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "external" },
+ });
+
+ mutate([`/private/`], null);
+ };
+
+ const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ await request(`/private/auth`, {
+ method: "POST",
+ token: currentToken,
+ data: { method: "token", token: newToken },
+ });
+
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
+ mutate([`/private/`], null);
+ };
+
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+}
+
+export function useInstanceDetails(): HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ revalidateIfStale: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+type KYCStatus =
+ | { type: "ok" }
+ | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects };
+
+export function useInstanceKYCDetails(): HttpResponse<
+ KYCStatus,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error } = useSWR<
+ HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/kyc`], fetcher, {
+ refreshInterval: 60 * 1000,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ revalidateOnMount: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (data) {
+ if (data.info?.status === 202)
+ return { ok: true, data: { type: "redirect", status: data.data } };
+ return { ok: true, data: { type: "ok" } };
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useManagedInstanceDetails(
+ instanceId: string,
+): HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/management/instances/${instanceId}`], request, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useBackendInstances(): HttpResponse<
+ MerchantBackend.Instances.InstancesResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { request } = useBackendBaseRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(["/management/instances"], request);
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/listener.ts b/packages/auditor-backoffice-ui/src/hooks/listener.ts
new file mode 100644
index 000000000..d101f7bb8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/listener.ts
@@ -0,0 +1,85 @@
+/*
+ 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)
+ */
+
+import { useState } from "preact/hooks";
+
+/**
+ * This component is used when a component wants one child to have a trigger for
+ * an action (a button) and other child have the action implemented (like
+ * gathering information with a form). The difference with other approaches is
+ * that in this case the parent component is not holding the state.
+ *
+ * It will return a subscriber and activator.
+ *
+ * The activator may be undefined, if it is undefined it is indicating that the
+ * subscriber is not ready to be called.
+ *
+ * The subscriber will receive a function (the listener) that will be call when the
+ * activator runs. The listener must return the collected information.
+ *
+ * As a result, when the activator is triggered by a child component, the
+ * @action function is called receives the information from the listener defined by other
+ * child component
+ *
+ * @param action from <T> to <R>
+ * @returns activator and subscriber, undefined activator means that there is not subscriber
+ */
+
+export function useListener<T, R = any>(
+ action: (r: T) => Promise<R>,
+): [undefined | (() => Promise<R>), (listener?: () => T) => void] {
+ type RunnerHandler = { toBeRan?: () => Promise<R> };
+ const [state, setState] = useState<RunnerHandler>({});
+
+ /**
+ * subscriber will receive a method that will be call when the activator runs
+ *
+ * @param listener function to be run when the activator runs
+ */
+ const subscriber = (listener?: () => T) => {
+ if (listener) {
+ setState({
+ toBeRan: () => {
+ const whatWeGetFromTheListener = listener();
+ return action(whatWeGetFromTheListener);
+ },
+ });
+ } else {
+ setState({
+ toBeRan: undefined,
+ });
+ }
+ };
+
+ /**
+ * activator will call runner if there is someone subscribed
+ */
+ const activator = state.toBeRan
+ ? async () => {
+ if (state.toBeRan) {
+ return state.toBeRan();
+ }
+ return Promise.reject();
+ }
+ : undefined;
+
+ return [activator, subscriber];
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/notifications.ts b/packages/auditor-backoffice-ui/src/hooks/notifications.ts
new file mode 100644
index 000000000..133ddd80b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/notifications.ts
@@ -0,0 +1,56 @@
+/*
+ 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)
+ */
+
+import { useState } from "preact/hooks";
+import { Notification } from "../utils/types.js";
+
+interface Result {
+ notifications: Notification[];
+ pushNotification: (n: Notification) => void;
+ removeNotification: (n: Notification) => void;
+}
+
+type NotificationWithDate = Notification & { since: Date };
+
+export function useNotifications(
+ initial: Notification[] = [],
+ timeout = 3000,
+): Result {
+ const [notifications, setNotifications] = useState<NotificationWithDate[]>(
+ initial.map((i) => ({ ...i, since: new Date() })),
+ );
+
+ const pushNotification = (n: Notification): void => {
+ const entry = { ...n, since: new Date() };
+ setNotifications((ns) => [...ns, entry]);
+ if (n.type !== "ERROR")
+ setTimeout(() => {
+ setNotifications((ns) => ns.filter((x) => x.since !== entry.since));
+ }, timeout);
+ };
+
+ const removeNotification = (notif: Notification) => {
+ setNotifications((ns: NotificationWithDate[]) =>
+ ns.filter((n) => n !== notif),
+ );
+ };
+ return { notifications, pushNotification, removeNotification };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts
new file mode 100644
index 000000000..c243309a8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/order.test.ts
@@ -0,0 +1,587 @@
+/*
+ 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)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_ORDER,
+ API_DELETE_ORDER,
+ API_FORGET_ORDER_BY_ID,
+ API_GET_ORDER_BY_ID,
+ API_LIST_ORDERS,
+ API_REFUND_ORDER_BY_ID,
+} from "./urls.js";
+
+describe("order api interaction with listing", () => {
+ it("should evict cache when creating an order", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+
+ env.addRequestExpectation(API_CREATE_ORDER, {
+ request: {
+ order: { amount: "ARS:12", summary: "pay me" },
+ },
+ response: { order_id: "3" },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as any, { order_id: "3" } as any],
+ },
+ });
+
+ api.createOrder({
+ order: { amount: "ARS:12", summary: "pay me" },
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when doing a refund", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: { orders: [{
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: true,
+ } as MerchantBackend.Orders.OrderHistoryEntry] },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ {
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: true,
+ },
+ ],
+ });
+ env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
+ request: {
+ reason: "double pay",
+ refund: "EUR:1",
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: { orders: [
+ { order_id: "1", amount: "EUR:12", refundable: false } as any,
+ ] },
+ });
+
+ api.refundOrder("1", {
+ reason: "double pay",
+ refund: "EUR:1",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ {
+ order_id: "1",
+ amount: "EUR:12",
+ refundable: false,
+ },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting an order", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceOrders({ paid: "yes" }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+
+ env.addRequestExpectation(API_DELETE_ORDER("1"), {});
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, paid: "yes" },
+ response: {
+ orders: [{ order_id: "2" } as any],
+ },
+ });
+
+ api.deleteOrder("1");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "2" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("order api interaction with details", () => {
+ it("should evict cache when doing a refund", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ // qparam: { delta: 0, paid: "yes" },
+ response: {
+ summary: "description",
+ refund_amount: "EUR:0",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:0",
+ });
+ env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
+ request: {
+ reason: "double pay",
+ refund: "EUR:1",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ response: {
+ summary: "description",
+ refund_amount: "EUR:1",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ api.refundOrder("1", {
+ reason: "double pay",
+ refund: "EUR:1",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:1",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when doing a forget", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ // qparam: { delta: 0, paid: "yes" },
+ response: {
+ summary: "description",
+ refund_amount: "EUR:0",
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useOrderDetails("1");
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: "description",
+ refund_amount: "EUR:0",
+ });
+ env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), {
+ request: {
+ fields: ["$.summary"],
+ },
+ });
+
+ env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), {
+ response: {
+ summary: undefined,
+ } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
+ });
+
+ api.forgetOrder("1", {
+ fields: ["$.summary"],
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ summary: undefined,
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("order listing pagination", () => {
+ it("should not load more if has reach the end", async () => {
+ const env = new ApiMockEnvironment();
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 20, wired: "yes", date_s: 12 },
+ response: {
+ orders: [{ order_id: "1" } as any],
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, wired: "yes", date_s: 13 },
+ response: {
+ orders: [{ order_id: "2" } as any],
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: "yes", date }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [{ order_id: "1" }, { order_id: "2" }],
+ });
+ expect(query.isReachingEnd).true;
+ expect(query.isReachingStart).true;
+
+ // should not trigger new state update or query
+ query.loadMore();
+ query.loadMorePrev();
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGE_SIZE", async () => {
+ const env = new ApiMockEnvironment();
+
+ const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
+ order_id: String(i),
+ }));
+ const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
+ order_id: String(i + 20),
+ }));
+ const ordersFrom20to0 = [...ordersFrom0to20].reverse();
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 20, wired: "yes", date_s: 12 },
+ response: {
+ orders: ordersFrom0to20,
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -20, wired: "yes", date_s: 13 },
+ response: {
+ orders: ordersFrom20to40,
+ },
+ });
+
+ const newDate = (d: Date) => {
+ //console.log("new date", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const date = new Date(12000);
+ const query = useInstanceOrders({ wired: "yes", date }, newDate);
+ const api = useOrderAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [...ordersFrom20to0, ...ordersFrom20to40],
+ });
+ expect(query.isReachingEnd).false;
+ expect(query.isReachingStart).false;
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: -40, wired: "yes", date_s: 13 },
+ response: {
+ orders: [...ordersFrom20to40, { order_id: "41" }],
+ },
+ });
+
+ query.loadMore();
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ ...ordersFrom20to0,
+ ...ordersFrom20to40,
+ { order_id: "41" },
+ ],
+ });
+
+ env.addRequestExpectation(API_LIST_ORDERS, {
+ qparam: { delta: 40, wired: "yes", date_s: 12 },
+ response: {
+ orders: [...ordersFrom0to20, { order_id: "-1" }],
+ },
+ });
+
+ query.loadMorePrev();
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ orders: [
+ { order_id: "-1" },
+ ...ordersFrom20to0,
+ ...ordersFrom20to40,
+ { order_id: "41" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/order.ts b/packages/auditor-backoffice-ui/src/hooks/order.ts
new file mode 100644
index 000000000..e7a893f2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/order.ts
@@ -0,0 +1,289 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface OrderAPI {
+ //FIXME: add OutOfStockResponse on 410
+ createOrder: (
+ data: MerchantBackend.Orders.PostOrderRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>;
+ forgetOrder: (
+ id: string,
+ data: MerchantBackend.Orders.ForgetRequest,
+ ) => Promise<HttpResponseOk<void>>;
+ refundOrder: (
+ id: string,
+ data: MerchantBackend.Orders.RefundRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>;
+ deleteOrder: (id: string) => Promise<HttpResponseOk<void>>;
+ getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>;
+}
+
+type YesOrNo = "yes" | "no";
+
+export function useOrderAPI(): OrderAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createOrder = async (
+ data: MerchantBackend.Orders.PostOrderRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
+ const res = await request<MerchantBackend.Orders.PostOrderResponse>(
+ `/private/orders`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+ await mutateAll(/.*private\/orders.*/);
+ // mutate('')
+ return res;
+ };
+ const refundOrder = async (
+ orderId: string,
+ data: MerchantBackend.Orders.RefundRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<MerchantBackend.Orders.MerchantRefundResponse>(
+ `/private/orders/${orderId}/refund`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ // order list returns refundable information, so we must evict everything
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+
+ const forgetOrder = async (
+ orderId: string,
+ data: MerchantBackend.Orders.ForgetRequest,
+ ): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<void>(`/private/orders/${orderId}/forget`, {
+ method: "PATCH",
+ data,
+ });
+ // we may be forgetting some fields that are pare of the listing, so we must evict everything
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+ const deleteOrder = async (
+ orderId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ mutateAll(/@"\/private\/orders"@/);
+ const res = request<void>(`/private/orders/${orderId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/orders.*/);
+ return res;
+ };
+
+ const getPaymentURL = async (
+ orderId: string,
+ ): Promise<HttpResponseOk<string>> => {
+ return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(
+ `/private/orders/${orderId}`,
+ {
+ method: "GET",
+ },
+ ).then((res) => {
+ const url =
+ res.data.order_status === "unpaid"
+ ? res.data.taler_pay_uri
+ : res.data.contract_terms.fulfillment_url;
+ const response: HttpResponseOk<string> = res as any;
+ response.data = url || "";
+ return response;
+ });
+ };
+
+ return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL };
+}
+
+export function useOrderDetails(
+ oderId: string,
+): HttpResponse<
+ MerchantBackend.Orders.MerchantOrderStatusResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/orders/${oderId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export interface InstanceOrderFilter {
+ paid?: YesOrNo;
+ refunded?: YesOrNo;
+ wired?: YesOrNo;
+ date?: Date;
+}
+
+export function useInstanceOrders(
+ args?: InstanceOrderFilter,
+ updateFilter?: (d: Date) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+> {
+ const { orderFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/orders`,
+ args?.paid,
+ args?.refunded,
+ args?.wired,
+ args?.date,
+ totalBefore,
+ ],
+ orderFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Orders.OrderHistory>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/orders`,
+ args?.paid,
+ args?.refunded,
+ args?.wired,
+ args?.date,
+ -totalAfter,
+ ],
+ orderFetcher,
+ );
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Orders.OrderHistory,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd = afterData && afterData.data.orders.length < totalAfter;
+ const isReachingStart =
+ args?.date === undefined ||
+ (beforeData && beforeData.data.orders.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.orders.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from =
+ afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s;
+ if (from && from !== "never" && updateFilter)
+ updateFilter(new Date(from * 1000));
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.orders.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from =
+ beforeData.data.orders[beforeData.data.orders.length - 1].timestamp
+ .t_s;
+ if (from && from !== "never" && updateFilter)
+ updateFilter(new Date(from * 1000));
+ }
+ },
+ };
+
+ const orders =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.orders
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.orders);
+ if (loadingAfter || loadingBefore) return { loading: true, data: { orders } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { orders }, ...pagination };
+ }
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/otp.ts b/packages/auditor-backoffice-ui/src/hooks/otp.ts
new file mode 100644
index 000000000..b045e365a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/otp.ts
@@ -0,0 +1,223 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = {
+ "1": {
+ otp_device_description: "first device",
+ otp_algorithm: 1,
+ otp_device_id: "1",
+ otp_key: "123",
+ },
+ "2": {
+ otp_device_description: "second device",
+ otp_algorithm: 0,
+ otp_device_id: "2",
+ otp_key: "456",
+ }
+}
+
+export function useOtpDeviceAPI(): OtpDeviceAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createOtpDevice = async (
+ data: MerchantBackend.OTP.OtpDeviceAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_DEVICES[data.otp_device_id] = data
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ const updateOtpDevice = async (
+ deviceId: string,
+ data: MerchantBackend.OTP.OtpDevicePatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm
+ // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr
+ // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description
+ // MOCKED_DEVICES[deviceId].otp_key = data.otp_key
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices/${deviceId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ const deleteOtpDevice = async (
+ deviceId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ // delete MOCKED_DEVICES[deviceId]
+ // return Promise.resolve({ ok: true, data: undefined });
+ const res = await request<void>(`/private/otp-devices/${deviceId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/otp-devices.*/);
+ return res;
+ };
+
+ return {
+ createOtpDevice,
+ updateOtpDevice,
+ deleteOtpDevice,
+ };
+}
+
+export interface OtpDeviceAPI {
+ createOtpDevice: (
+ data: MerchantBackend.OTP.OtpDeviceAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateOtpDevice: (
+ id: string,
+ data: MerchantBackend.OTP.OtpDevicePatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceOtpDeviceFilter {
+}
+
+export function useInstanceOtpDevices(
+ args?: InstanceOtpDeviceFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.OTP.OtpDeviceSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // loadMore: () => { },
+ // loadMorePrev: () => { },
+ // data: {
+ // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({
+ // device_description: d.otp_device_description,
+ // otp_device_id: d.otp_device_id
+ // }))
+ // }
+ // }
+
+ const { fetcher } = useBackendInstanceRequest();
+
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/otp-devices`], fetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.OTP.OtpDeviceSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData /*, beforeData*/]);
+
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.otp_devices.length < totalAfter;
+ const isReachingStart = true;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1]
+ .otp_device_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ },
+ };
+
+ const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices;
+ if (loadingAfter /* || loadingBefore */)
+ return { loading: true, data: { otp_devices } };
+ if (/*beforeData &&*/ afterData) {
+ return { ok: true, data: { otp_devices }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useOtpDeviceDetails(
+ deviceId: string,
+): HttpResponse<
+ MerchantBackend.OTP.OtpDeviceDetails,
+ MerchantBackend.ErrorDetail
+> {
+ // return {
+ // ok: true,
+ // data: {
+ // device_description: MOCKED_DEVICES[deviceId].otp_device_description,
+ // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm,
+ // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr
+ // }
+ // }
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/otp-devices/${deviceId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/product.test.ts b/packages/auditor-backoffice-ui/src/hooks/product.test.ts
new file mode 100644
index 000000000..7cac10e25
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/product.test.ts
@@ -0,0 +1,362 @@
+/*
+ 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)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import {
+ useInstanceProducts,
+ useProductAPI,
+ useProductDetails,
+} from "./product.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_CREATE_PRODUCT,
+ API_DELETE_PRODUCT,
+ API_GET_PRODUCT_BY_ID,
+ API_LIST_PRODUCTS,
+ API_UPDATE_PRODUCT_BY_ID,
+} from "./urls.js";
+
+describe("product api interaction with listing", () => {
+ it("should evict cache when creating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+
+ env.addRequestExpectation(API_CREATE_PRODUCT, {
+ request: {
+ price: "ARS:23",
+ } as MerchantBackend.Products.ProductAddDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }, { product_id: "2345" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: {
+ price: "ARS:23",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.createProduct({
+ price: "ARS:23",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ {
+ id: "1234",
+ price: "ARS:12",
+ },
+ {
+ id: "2345",
+ price: "ARS:23",
+ },
+ ]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when updating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+
+ env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
+ request: {
+ price: "ARS:13",
+ } as MerchantBackend.Products.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:13",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.updateProduct("1234", {
+ price: "ARS:13",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ {
+ id: "1234",
+ price: "ARS:13",
+ },
+ ]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }, { product_id: "2345" }],
+ },
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
+ });
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
+ response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceProducts();
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([
+ { id: "1234", price: "ARS:12" },
+ { id: "2345", price: "ARS:23" },
+ ]);
+
+ env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {});
+
+ env.addRequestExpectation(API_LIST_PRODUCTS, {
+ response: {
+ products: [{ product_id: "1234" }],
+ },
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
+ response: {
+ price: "ARS:12",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+ api.deleteProduct("2345");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("product api interaction with details", () => {
+ it("should evict cache when updating a product", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+ response: {
+ description: "this is a description",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useProductDetails("12");
+ const api = useProductAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ description: "this is a description",
+ });
+
+ env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
+ request: {
+ description: "other description",
+ } as MerchantBackend.Products.ProductPatchDetail,
+ });
+
+ env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
+ response: {
+ description: "other description",
+ } as MerchantBackend.Products.ProductDetail,
+ });
+
+ api.updateProduct("12", {
+ description: "other description",
+ } as any);
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ description: "other description",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/product.ts b/packages/auditor-backoffice-ui/src/hooks/product.ts
new file mode 100644
index 000000000..b8f55cb77
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/product.ts
@@ -0,0 +1,177 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface ProductAPI {
+ getProduct: (
+ id: string,
+ ) => Promise<void>;
+ createProduct: (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ) => Promise<void>;
+ updateProduct: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ deleteProduct: (id: string) => Promise<void>;
+ lockProduct: (
+ id: string,
+ data: MerchantBackend.Products.LockRequest,
+ ) => Promise<void>;
+}
+
+export function useProductAPI(): ProductAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+
+ const { request } = useBackendInstanceRequest();
+
+ const createProduct = async (
+ data: MerchantBackend.Products.ProductAddDetail,
+ ): Promise<void> => {
+ const res = await request(`/private/products`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const updateProduct = async (
+ productId: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ): Promise<void> => {
+ const r = await request(`/private/products/${productId}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*\/private\/products.*/);
+ };
+
+ const deleteProduct = async (productId: string): Promise<void> => {
+ await request(`/private/products/${productId}`, {
+ method: "DELETE",
+ });
+ await mutate([`/private/products`]);
+ };
+
+ const lockProduct = async (
+ productId: string,
+ data: MerchantBackend.Products.LockRequest,
+ ): Promise<void> => {
+ await request(`/private/products/${productId}/lock`, {
+ method: "POST",
+ data,
+ });
+
+ return await mutateAll(/.*"\/private\/products.*/);
+ };
+
+ const getProduct = async (
+ productId: string,
+ ): Promise<void> => {
+ await request(`/private/products/${productId}`, {
+ method: "GET",
+ });
+
+ return
+ };
+
+ return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct };
+}
+
+export function useInstanceProducts(): HttpResponse<
+ (MerchantBackend.Products.ProductDetail & WithId)[],
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher, multiFetcher } = useBackendInstanceRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/products`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ const paths = (list?.data.products || []).map(
+ (p) => `/deposit-confirmation/${p.serial_id}`,
+ );
+ const { data: products, error: productError } = useSWR<
+ HttpResponseOk<AuditorBackend.DepositConfirmation.DepositConfirmationDetail>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >([paths], multiFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+ if (productError) return productError.cause;
+
+ if (products) {
+ const dataWithId = products.map((d) => {
+ //take the id from the queried url
+ return {
+ ...d.data,
+ id: d.info?.url.replace(/.*\/private\/products\//, "") || "",
+ };
+ });
+ return { ok: true, data: dataWithId };
+ }
+ return { loading: true };
+}
+
+export function useProductDetails(
+ productId: string,
+): HttpResponse<
+ MerchantBackend.Products.ProductDetail,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Products.ProductDetail>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/products/${productId}`], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts
new file mode 100644
index 000000000..b3eecd754
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts
@@ -0,0 +1,448 @@
+/*
+ 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)
+ */
+
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import {
+ useInstanceReserves,
+ useReserveDetails,
+ useReservesAPI,
+ useRewardDetails,
+} from "./reserves.js";
+import { ApiMockEnvironment } from "./testing.js";
+import {
+ API_AUTHORIZE_REWARD,
+ API_AUTHORIZE_REWARD_FOR_RESERVE,
+ API_CREATE_RESERVE,
+ API_DELETE_RESERVE,
+ API_GET_RESERVE_BY_ID,
+ API_GET_REWARD_BY_ID,
+ API_LIST_RESERVES,
+} from "./urls.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+describe("reserve api interaction with listing", () => {
+ it("should evict cache when creating a reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useInstanceReserves();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [{ reserve_pub: "11" }],
+ });
+
+ env.addRequestExpectation(API_CREATE_RESERVE, {
+ request: {
+ initial_balance: "ARS:3333",
+ exchange_url: "http://url",
+ wire_method: "iban",
+ },
+ response: {
+ reserve_pub: "22",
+ accounts: [],
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ api.createReserve({
+ initial_balance: "ARS:3333",
+ exchange_url: "http://url",
+ wire_method: "iban",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when deleting a reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "11",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "33",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useInstanceReserves();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [
+ { reserve_pub: "11" },
+ { reserve_pub: "22" },
+ { reserve_pub: "33" },
+ ],
+ });
+
+ env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
+ env.addRequestExpectation(API_LIST_RESERVES, {
+ response: {
+ reserves: [
+ {
+ reserve_pub: "22",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ {
+ reserve_pub: "33",
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
+ ],
+ },
+ });
+
+ api.deleteReserve("11");
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("reserve api interaction with details", () => {
+ it("should evict cache when adding a reward for a specific reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useReserveDetails("11");
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ });
+
+ env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), {
+ request: {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ },
+ response: {
+ reward_id: "id2",
+ taler_reward_uri: "uri",
+ reward_expiration: { t_s: 1 },
+ reward_status_url: "url",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ api.authorizeRewardReserve("11", {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ it("should evict cache when adding a reward for a random reserve", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const api = useReservesAPI();
+ const query = useReserveDetails("11");
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ });
+
+ env.addRequestExpectation(API_AUTHORIZE_REWARD, {
+ request: {
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ },
+ response: {
+ reward_id: "id2",
+ taler_reward_uri: "uri",
+ reward_expiration: { t_s: 1 },
+ reward_status_url: "url",
+ },
+ });
+
+ env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
+ response: {
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ } as MerchantBackend.Rewards.ReserveDetail,
+ qparam: {
+ rewards: "yes",
+ },
+ });
+
+ api.authorizeReward({
+ amount: "USD:12",
+ justification: "not",
+ next_url: "http://taler.net",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ accounts: [{ payto_uri: "payto://here" }],
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
+ ],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("reserve api interaction with reward details", () => {
+ it("should list rewards", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), {
+ response: {
+ total_picked_up: "USD:12",
+ reason: "not",
+ } as MerchantBackend.Rewards.RewardDetails,
+ qparam: {
+ pickups: "yes",
+ },
+ });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useRewardDetails("11");
+ return { query };
+ },
+ {},
+ [
+ ({ query }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ ({ query }) => {
+ expect(query.loading).false;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ total_picked_up: "USD:12",
+ reason: "not",
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/reserves.ts b/packages/auditor-backoffice-ui/src/hooks/reserves.ts
new file mode 100644
index 000000000..b719bfbe6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/reserves.ts
@@ -0,0 +1,181 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { MerchantBackend } from "../declaration.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useReservesAPI(): ReserveMutateAPI {
+ const mutateAll = useMatchMutate();
+ const { mutate } = useSWRConfig();
+ const { request } = useBackendInstanceRequest();
+
+ const createReserve = async (
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
+ ): Promise<
+ HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>
+ > => {
+ const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>(
+ `/private/reserves`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict reserve list query
+ await mutateAll(/.*private\/reserves.*/);
+
+ return res;
+ };
+
+ const authorizeRewardReserve = async (
+ pub: string,
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/reserves/${pub}/authorize-reward`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict reserve details query
+ await mutate([`/private/reserves/${pub}`]);
+
+ return res;
+ };
+
+ const authorizeReward = async (
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/rewards`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+
+ //evict all details query
+ await mutateAll(/.*private\/reserves\/.*/);
+
+ return res;
+ };
+
+ const deleteReserve = async (
+ pub: string,
+ ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => {
+ const res = await request<void>(`/private/reserves/${pub}`, {
+ method: "DELETE",
+ });
+
+ //evict reserve list query
+ await mutateAll(/.*private\/reserves.*/);
+
+ return res;
+ };
+
+ return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve };
+}
+
+export interface ReserveMutateAPI {
+ createReserve: (
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>;
+ authorizeRewardReserve: (
+ id: string,
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
+ authorizeReward: (
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
+ deleteReserve: (
+ id: string,
+ ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>;
+}
+
+export function useInstanceReserves(): HttpResponse<
+ MerchantBackend.Rewards.RewardReserveStatus,
+ MerchantBackend.ErrorDetail
+> {
+ const { fetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/reserves`], fetcher);
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useReserveDetails(
+ reserveId: string,
+): HttpResponse<
+ MerchantBackend.Rewards.ReserveDetail,
+ MerchantBackend.ErrorDetail
+> {
+ const { reserveDetailFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
+
+export function useRewardDetails(
+ rewardId: string,
+): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> {
+ const { rewardsDetailFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Rewards.RewardDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/templates.ts b/packages/auditor-backoffice-ui/src/hooks/templates.ts
new file mode 100644
index 000000000..ee8728cc8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/templates.ts
@@ -0,0 +1,266 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useTemplateAPI(): TemplateAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createTemplate = async (
+ data: MerchantBackend.Template.TemplateAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const updateTemplate = async (
+ templateId: string,
+ data: MerchantBackend.Template.TemplatePatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const deleteTemplate = async (
+ templateId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const createOrderFromTemplate = async (
+ templateId: string,
+ data: MerchantBackend.Template.UsingTemplateDetails,
+ ): Promise<
+ HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>
+ > => {
+ const res = await request<MerchantBackend.Template.UsingTemplateResponse>(
+ `/templates/${templateId}`,
+ {
+ method: "POST",
+ data,
+ },
+ );
+ await mutateAll(/.*private\/templates.*/);
+ return res;
+ };
+
+ const testTemplateExist = async (
+ templateId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", });
+ return res;
+ };
+
+
+ return {
+ createTemplate,
+ updateTemplate,
+ deleteTemplate,
+ testTemplateExist,
+ createOrderFromTemplate,
+ };
+}
+
+export interface TemplateAPI {
+ createTemplate: (
+ data: MerchantBackend.Template.TemplateAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateTemplate: (
+ id: string,
+ data: MerchantBackend.Template.TemplatePatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ testTemplateExist: (
+ id: string
+ ) => Promise<HttpResponseOk<void>>;
+ deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
+ createOrderFromTemplate: (
+ id: string,
+ data: MerchantBackend.Template.UsingTemplateDetails,
+ ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>;
+}
+
+export interface InstanceTemplateFilter {
+ //FIXME: add filter to the template list
+ position?: string;
+}
+
+export function useInstanceTemplates(
+ args?: InstanceTemplateFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { templateFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>>(
+ [
+ `/private/templates`,
+ args?.position,
+ totalBefore,
+ ],
+ templateFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/templates`, args?.position, -totalAfter], templateFetcher);
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Template.TemplateSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.templates.length < totalAfter;
+ const isReachingStart = args?.position === undefined
+ ||
+ (beforeData && beforeData.data.templates.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.templates.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${afterData.data.templates[afterData.data.templates.length - 1]
+ .template_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.templates.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from = `${beforeData.data.templates[beforeData.data.templates.length - 1]
+ .template_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ };
+
+ // const templates = !afterData ? [] : (afterData || lastAfter).data.templates;
+ const templates =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.templates
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.templates);
+ if (loadingAfter || loadingBefore)
+ return { loading: true, data: { templates } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { templates }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useTemplateDetails(
+ templateId: string,
+): HttpResponse<
+ MerchantBackend.Template.TemplateDetails,
+ MerchantBackend.ErrorDetail
+> {
+ const { templateFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Template.TemplateDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/templates/${templateId}`], templateFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) {
+ return data;
+ }
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
new file mode 100644
index 000000000..3ea22475b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
@@ -0,0 +1,180 @@
+/*
+ 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)
+ */
+
+import { MockEnvironment } from "@gnu-taler/web-util/testing";
+import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
+import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http";
+import { SWRConfig } from "swr";
+import { ApiContextProvider } from "@gnu-taler/web-util/browser";
+import { BackendContextProvider } from "../context/backend.js";
+import { InstanceContextProvider } from "../context/instance.js";
+import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser";
+import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient } from "@gnu-taler/taler-util";
+
+export class ApiMockEnvironment extends MockEnvironment {
+ constructor(debug = false) {
+ super(debug);
+ }
+
+ mockApiIfNeeded(): void {
+ null; // do nothing
+ }
+
+ public buildTestingContext(): FunctionalComponent<{
+ children: ComponentChildren;
+ }> {
+ const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE =
+ this.saveRequestAndGetMockedResponse.bind(this);
+
+ return function TestingContext({
+ children,
+ }: {
+ children: ComponentChildren;
+ }): VNode {
+
+ async function request<T>(
+ base: string,
+ path: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ const _url = new URL(`${base}${path}`);
+ // Object.entries(options.params ?? {}).forEach(([key, value]) => {
+ // _url.searchParams.set(key, String(value));
+ // });
+
+ const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE(
+ {
+ method: options.method ?? "GET",
+ url: _url.href,
+ },
+ {
+ qparam: options.params,
+ auth: options.token,
+ request: options.data,
+ },
+ );
+ const status = mocked.expectedQuery?.query.code ?? 200;
+ const requestPayload = mocked.expectedQuery?.params?.request;
+ const responsePayload = mocked.expectedQuery?.params?.response;
+
+ return {
+ ok: true,
+ data: responsePayload as T,
+ loading: false,
+ clientError: false,
+ serverError: false,
+ info: {
+ hasToken: !!options.token,
+ status,
+ url: _url.href,
+ payload: options.data,
+ options: {},
+ },
+ };
+ }
+ const SC: any = SWRConfig;
+
+ const mockHttpClient = new class implements HttpRequestLibrary {
+ async fetch(url: string, options?: HttpRequestOptions | undefined): Promise<HttpResponse> {
+ const _url = new URL(url);
+ const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE(
+ {
+ method: options?.method ?? "GET",
+ url: _url.href,
+ },
+ {
+ qparam: _url.searchParams,
+ auth: options as any,
+ request: options?.body as any,
+ },
+ );
+ const status = mocked.expectedQuery?.query.code ?? 200;
+ const requestPayload = mocked.expectedQuery?.params?.request;
+ const responsePayload = mocked.expectedQuery?.params?.response;
+
+ // FIXME: complete this implementation to mock any query
+ const resp: HttpResponse = {
+ requestUrl: _url.href,
+ status: status,
+ headers: {} as any,
+ requestMethod: options?.method ?? "GET",
+ json: async () => responsePayload,
+ text: async () => responsePayload as any as string,
+ bytes: async () => responsePayload as ArrayBuffer,
+ };
+ return resp
+ }
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "GET",
+ ...opt,
+ });
+ }
+
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ return this.fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ ...opt,
+ });
+ }
+
+ }
+ const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient)
+ const bankIntegration = bankCore.getIntegrationAPI()
+ const bankRevenue = bankCore.getRevenueAPI("a")
+ const bankWire = bankCore.getWireGatewayAPI("b")
+
+ return (
+ <BackendContextProvider defaultUrl="http://backend">
+ <InstanceContextProvider
+ value={{
+ token: undefined,
+ id: "default",
+ admin: true,
+ changeToken: () => null,
+ }}
+ >
+ <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}>
+ <SC
+ value={{
+ loadingTimeout: 0,
+ dedupingInterval: 0,
+ shouldRetryOnError: false,
+ errorRetryInterval: 0,
+ errorRetryCount: 0,
+ provider: () => new Map(),
+ }}
+ >
+ {children}
+ </SC>
+ </ApiContextProvider>
+ </InstanceContextProvider>
+ </BackendContextProvider>
+ );
+ };
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts
new file mode 100644
index 000000000..a7187af27
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts
@@ -0,0 +1,254 @@
+/*
+ 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)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { MerchantBackend } from "../declaration.js";
+import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js";
+import { ApiMockEnvironment } from "./testing.js";
+import { useInstanceTransfers, useTransferAPI } from "./transfer.js";
+
+describe("transfer api interaction with listing", () => {
+ it("should evict cache when informing a transfer", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20 },
+ response: {
+ transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails],
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const query = useInstanceTransfers({}, moveCursor);
+ const api = useTransferAPI();
+ return { query, api };
+ },
+ {},
+ [
+ ({ query, api }) => {
+ expect(query.loading).true;
+ },
+
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "2" }],
+ });
+
+ env.addRequestExpectation(API_INFORM_TRANSFERS, {
+ request: {
+ wtid: "3",
+ credit_amount: "EUR:1",
+ exchange_url: "exchange.url",
+ payto_uri: "payto://",
+ },
+ response: { total: "" } as any,
+ });
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20 },
+ response: {
+ transfers: [{ wtid: "3" } as any, { wtid: "2" } as any],
+ },
+ });
+
+ api.informTransfer({
+ wtid: "3",
+ credit_amount: "EUR:1",
+ exchange_url: "exchange.url",
+ payto_uri: "payto://",
+ });
+ },
+ ({ query, api }) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "3" }, { wtid: "2" }],
+ });
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
+
+describe("transfer listing pagination", () => {
+ it("should not load more if has reach the end", async () => {
+ const env = new ApiMockEnvironment();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20, payto_uri: "payto://" },
+ response: {
+ transfers: [{ wtid: "2" }, { wtid: "1" } as any],
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor);
+ },
+ {},
+ [
+ (query) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(query.loading).true;
+ },
+ (query) => {
+ expect(query.loading).undefined;
+ expect(query.ok).true;
+ if (!query.ok) return;
+ expect(query.data).deep.equals({
+ transfers: [{ wtid: "2" }, { wtid: "1" }],
+ });
+ expect(query.isReachingEnd).true;
+ expect(query.isReachingStart).true;
+
+ //check that this button won't trigger more updates since
+ //has reach end and start
+ query.loadMore();
+ query.loadMorePrev();
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+
+ it("should load more if result brings more that PAGE_SIZE", async () => {
+ const env = new ApiMockEnvironment();
+
+ const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
+ wtid: String(i),
+ }));
+ const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
+ wtid: String(i + 20),
+ }));
+ const transfersFrom20to0 = [...transfersFrom0to20].reverse();
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: 20, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: transfersFrom0to20,
+ },
+ });
+
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -20, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: transfersFrom20to40,
+ },
+ });
+
+ const moveCursor = (d: string) => {
+ console.log("new position", d);
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ return useInstanceTransfers(
+ { payto_uri: "payto://", position: "1" },
+ moveCursor,
+ );
+ },
+ {},
+ [
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).true;
+ },
+ (result) => {
+ expect(result.loading).undefined;
+ expect(result.ok).true;
+ if (!result.ok) return;
+ expect(result.data).deep.equals({
+ transfers: [...transfersFrom20to0, ...transfersFrom20to40],
+ });
+ expect(result.isReachingEnd).false;
+ expect(result.isReachingStart).false;
+
+ //query more
+ env.addRequestExpectation(API_LIST_TRANSFERS, {
+ qparam: { limit: -40, payto_uri: "payto://", offset: "1" },
+ response: {
+ transfers: [...transfersFrom20to40, { wtid: "41" }],
+ },
+ });
+ result.loadMore();
+ },
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).true;
+ },
+ (result) => {
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({
+ result: "ok",
+ });
+ expect(result.loading).undefined;
+ expect(result.ok).true;
+ if (!result.ok) return;
+ expect(result.data).deep.equals({
+ transfers: [
+ ...transfersFrom20to0,
+ ...transfersFrom20to40,
+ { wtid: "41" },
+ ],
+ });
+ expect(result.isReachingEnd).true;
+ expect(result.isReachingStart).false;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/hooks/transfer.ts b/packages/auditor-backoffice-ui/src/hooks/transfer.ts
new file mode 100644
index 000000000..27c3bdc75
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/transfer.ts
@@ -0,0 +1,188 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useTransferAPI(): TransferAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const informTransfer = async (
+ data: MerchantBackend.Transfers.TransferInformation,
+ ): Promise<HttpResponseOk<{}>> => {
+ const res = await request<{}>(`/private/transfers`, {
+ method: "POST",
+ data,
+ });
+
+ await mutateAll(/.*private\/transfers.*/);
+ return res;
+ };
+
+ return { informTransfer };
+}
+
+export interface TransferAPI {
+ informTransfer: (
+ data: MerchantBackend.Transfers.TransferInformation,
+ ) => Promise<HttpResponseOk<{}>>;
+}
+
+export interface InstanceTransferFilter {
+ payto_uri?: string;
+ verified?: "yes" | "no";
+ position?: string;
+}
+
+export function useInstanceTransfers(
+ args?: InstanceTransferFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+> {
+ const { transferFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0;
+
+ /**
+ * FIXME: this can be cleaned up a little
+ *
+ * the logic of double query should be inside the orderFetch so from the hook perspective and cache
+ * is just one query and one error status
+ */
+ const {
+ data: beforeData,
+ error: beforeError,
+ isValidating: loadingBefore,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/transfers`,
+ args?.payto_uri,
+ args?.verified,
+ args?.position,
+ totalBefore,
+ ],
+ transferFetcher,
+ );
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Transfers.TransferList>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >(
+ [
+ `/private/transfers`,
+ args?.payto_uri,
+ args?.verified,
+ args?.position,
+ -totalAfter,
+ ],
+ transferFetcher,
+ );
+
+ //this will save last result
+ const [lastBefore, setLastBefore] = useState<
+ HttpResponse<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Transfers.TransferList,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ if (beforeData) setLastBefore(beforeData);
+ }, [afterData, beforeData]);
+
+ if (beforeError) return beforeError.cause;
+ if (afterError) return afterError.cause;
+
+ // if the query returns less that we ask, then we have reach the end or beginning
+ const isReachingEnd =
+ afterData && afterData.data.transfers.length < totalAfter;
+ const isReachingStart =
+ args?.position === undefined ||
+ (beforeData && beforeData.data.transfers.length < totalBefore);
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.transfers.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${
+ afterData.data.transfers[afterData.data.transfers.length - 1]
+ .transfer_serial_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ if (!beforeData || isReachingStart) return;
+ if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {
+ setPageBefore(pageBefore + 1);
+ } else if (beforeData) {
+ const from = `${
+ beforeData.data.transfers[beforeData.data.transfers.length - 1]
+ .transfer_serial_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ };
+
+ const transfers =
+ !beforeData || !afterData
+ ? []
+ : (beforeData || lastBefore).data.transfers
+ .slice()
+ .reverse()
+ .concat((afterData || lastAfter).data.transfers);
+ if (loadingAfter || loadingBefore)
+ return { loading: true, data: { transfers } };
+ if (beforeData && afterData) {
+ return { ok: true, data: { transfers }, ...pagination };
+ }
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/urls.ts b/packages/auditor-backoffice-ui/src/hooks/urls.ts
new file mode 100644
index 000000000..b6485259f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/urls.ts
@@ -0,0 +1,303 @@
+/*
+ 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)
+ */
+import { Query } from "@gnu-taler/web-util/testing";
+import { MerchantBackend } from "../declaration.js";
+
+////////////////////
+// ORDER
+////////////////////
+
+export const API_CREATE_ORDER: Query<
+ MerchantBackend.Orders.PostOrderRequest,
+ MerchantBackend.Orders.PostOrderResponse
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_GET_ORDER_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+export const API_LIST_ORDERS: Query<
+ unknown,
+ MerchantBackend.Orders.OrderHistory
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/orders",
+};
+
+export const API_REFUND_ORDER_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Orders.RefundRequest,
+ MerchantBackend.Orders.MerchantRefundResponse
+> => ({
+ method: "POST",
+ url: `http://backend/instances/default/private/orders/${id}/refund`,
+});
+
+export const API_FORGET_ORDER_BY_ID = (
+ id: string,
+): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
+ method: "PATCH",
+ url: `http://backend/instances/default/private/orders/${id}/forget`,
+});
+
+export const API_DELETE_ORDER = (
+ id: string,
+): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/orders/${id}`,
+});
+
+////////////////////
+// TRANSFER
+////////////////////
+
+export const API_LIST_TRANSFERS: Query<
+ unknown,
+ MerchantBackend.Transfers.TransferList
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+export const API_INFORM_TRANSFERS: Query<
+ MerchantBackend.Transfers.TransferInformation,
+ {}
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/transfers",
+};
+
+////////////////////
+// PRODUCT
+////////////////////
+
+export const API_CREATE_PRODUCT: Query<
+ MerchantBackend.Products.ProductAddDetail,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_LIST_PRODUCTS: Query<
+ unknown,
+ MerchantBackend.Products.InventorySummaryResponse
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/products",
+};
+
+export const API_GET_PRODUCT_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Products.ProductDetail> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+export const API_UPDATE_PRODUCT_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Products.ProductPatchDetail,
+ MerchantBackend.Products.InventorySummaryResponse
+> => ({
+ method: "PATCH",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/products/${id}`,
+});
+
+////////////////////
+// RESERVES
+////////////////////
+
+export const API_CREATE_RESERVE: Query<
+ MerchantBackend.Rewards.ReserveCreateRequest,
+ MerchantBackend.Rewards.ReserveCreateConfirmation
+> = {
+ method: "POST",
+ url: "http://backend/instances/default/private/reserves",
+};
+export const API_LIST_RESERVES: Query<
+ unknown,
+ MerchantBackend.Rewards.RewardReserveStatus
+> = {
+ method: "GET",
+ url: "http://backend/instances/default/private/reserves",
+};
+
+export const API_GET_RESERVE_BY_ID = (
+ pub: string,
+): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/reserves/${pub}`,
+});
+
+export const API_GET_REWARD_BY_ID = (
+ pub: string,
+): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({
+ method: "GET",
+ url: `http://backend/instances/default/private/rewards/${pub}`,
+});
+
+export const API_AUTHORIZE_REWARD_FOR_RESERVE = (
+ pub: string,
+): Query<
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
+> => ({
+ method: "POST",
+ url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`,
+});
+
+export const API_AUTHORIZE_REWARD: Query<
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
+> = {
+ method: "POST",
+ url: `http://backend/instances/default/private/rewards`,
+};
+
+export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/instances/default/private/reserves/${id}`,
+});
+
+////////////////////
+// INSTANCE ADMIN
+////////////////////
+
+export const API_CREATE_INSTANCE: Query<
+ MerchantBackend.Instances.InstanceConfigurationMessage,
+ unknown
+> = {
+ method: "POST",
+ url: "http://backend/management/instances",
+};
+
+export const API_GET_INSTANCE_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_GET_INSTANCE_KYC_BY_ID = (
+ id: string,
+): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({
+ method: "GET",
+ url: `http://backend/management/instances/${id}/kyc`,
+});
+
+export const API_LIST_INSTANCES: Query<
+ unknown,
+ MerchantBackend.Instances.InstancesResponse
+> = {
+ method: "GET",
+ url: "http://backend/management/instances",
+};
+
+export const API_UPDATE_INSTANCE_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Instances.InstanceReconfigurationMessage,
+ unknown
+> => ({
+ method: "PATCH",
+ url: `http://backend/management/instances/${id}`,
+});
+
+export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
+ id: string,
+): Query<
+ MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ unknown
+> => ({
+ method: "POST",
+ url: `http://backend/management/instances/${id}/auth`,
+});
+
+export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({
+ method: "DELETE",
+ url: `http://backend/management/instances/${id}`,
+});
+
+////////////////////
+// AUTH
+////////////////////
+
+export const API_NEW_LOGIN: Query<
+ MerchantBackend.Instances.LoginTokenRequest,
+ unknown
+> = ({
+ method: "POST",
+ url: `http://backend/private/token`,
+});
+
+////////////////////
+// INSTANCE
+////////////////////
+
+export const API_GET_CURRENT_INSTANCE: Query<
+ unknown,
+ MerchantBackend.Instances.QueryInstancesResponse
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_GET_CURRENT_INSTANCE_KYC: Query<
+ unknown,
+ MerchantBackend.KYC.AccountKycRedirects
+> = {
+ method: "GET",
+ url: `http://backend/instances/default/private/kyc`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE: Query<
+ MerchantBackend.Instances.InstanceReconfigurationMessage,
+ unknown
+> = {
+ method: "PATCH",
+ url: `http://backend/instances/default/private/`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
+ MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+ unknown
+> = {
+ method: "POST",
+ url: `http://backend/instances/default/private/auth`,
+};
+
+export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = {
+ method: "DELETE",
+ url: `http://backend/instances/default/private`,
+};
diff --git a/packages/auditor-backoffice-ui/src/hooks/useSettings.ts b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts
new file mode 100644
index 000000000..8c1ebd9f6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/useSettings.ts
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForString,
+} from "@gnu-taler/taler-util";
+
+export interface Settings {
+ advanceOrderMode: boolean;
+ dateFormat: "ymd" | "dmy" | "mdy";
+}
+
+const defaultSettings: Settings = {
+ advanceOrderMode: false,
+ dateFormat: "ymd",
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("advanceOrderMode", codecForBoolean())
+ .property("dateFormat", codecForEither(
+ codecForConstString("ymd"),
+ codecForConstString("dmy"),
+ codecForConstString("mdy"),
+ ))
+ .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
+
+export function useSettings(): [
+ Readonly<Settings>,
+ (s: Settings) => void,
+] {
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
+
+ // const parsed: Settings = value ?? defaultSettings;
+ // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ // const next = { ...parsed, [k]: v }
+ // update(next);
+ // }
+ return [value, update];
+}
+
+export function dateFormatForSettings(s: Settings): string {
+ switch (s.dateFormat) {
+ case "ymd": return "yyyy/MM/dd"
+ case "dmy": return "dd/MM/yyyy"
+ case "mdy": return "MM/dd/yyyy"
+ }
+}
+
+export function datetimeFormatForSettings(s: Settings): string {
+ return dateFormatForSettings(s) + " HH:mm:ss"
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
new file mode 100644
index 000000000..ad6bf96e2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
@@ -0,0 +1,178 @@
+/*
+ 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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useWebhookAPI(): WebhookAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ const createWebhook = async (
+ data: MerchantBackend.Webhooks.WebhookAddDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks`, {
+ method: "POST",
+ data,
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ const updateWebhook = async (
+ webhookId: string,
+ data: MerchantBackend.Webhooks.WebhookPatchDetails,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks/${webhookId}`, {
+ method: "PATCH",
+ data,
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ const deleteWebhook = async (
+ webhookId: string,
+ ): Promise<HttpResponseOk<void>> => {
+ const res = await request<void>(`/private/webhooks/${webhookId}`, {
+ method: "DELETE",
+ });
+ await mutateAll(/.*private\/webhooks.*/);
+ return res;
+ };
+
+ return { createWebhook, updateWebhook, deleteWebhook };
+}
+
+export interface WebhookAPI {
+ createWebhook: (
+ data: MerchantBackend.Webhooks.WebhookAddDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ updateWebhook: (
+ id: string,
+ data: MerchantBackend.Webhooks.WebhookPatchDetails,
+ ) => Promise<HttpResponseOk<void>>;
+ deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>;
+}
+
+export interface InstanceWebhookFilter {
+ //FIXME: add filter to the webhook list
+ position?: string;
+}
+
+export function useInstanceWebhooks(
+ args?: InstanceWebhookFilter,
+ updatePosition?: (id: string) => void,
+): HttpResponsePaginated<
+ MerchantBackend.Webhooks.WebhookSummaryResponse,
+ MerchantBackend.ErrorDetail
+> {
+ const { webhookFetcher } = useBackendInstanceRequest();
+
+ const [pageBefore, setPageBefore] = useState(1);
+ const [pageAfter, setPageAfter] = useState(1);
+
+ const totalAfter = pageAfter * PAGE_SIZE;
+ const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
+
+ const {
+ data: afterData,
+ error: afterError,
+ isValidating: loadingAfter,
+ } = useSWR<
+ HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher);
+
+ const [lastAfter, setLastAfter] = useState<
+ HttpResponse<
+ MerchantBackend.Webhooks.WebhookSummaryResponse,
+ MerchantBackend.ErrorDetail
+ >
+ >({ loading: true });
+ useEffect(() => {
+ if (afterData) setLastAfter(afterData);
+ }, [afterData]);
+
+ if (afterError) return afterError.cause;
+
+ const isReachingEnd =
+ afterData && afterData.data.webhooks.length < totalAfter;
+ const isReachingStart = true;
+
+ const pagination = {
+ isReachingEnd,
+ isReachingStart,
+ loadMore: () => {
+ if (!afterData || isReachingEnd) return;
+ if (afterData.data.webhooks.length < MAX_RESULT_SIZE) {
+ setPageAfter(pageAfter + 1);
+ } else {
+ const from = `${
+ afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id
+ }`;
+ if (from && updatePosition) updatePosition(from);
+ }
+ },
+ loadMorePrev: () => {
+ return;
+ },
+ };
+
+ const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks;
+
+ if (loadingAfter) return { loading: true, data: { webhooks } };
+ if (afterData) {
+ return { ok: true, data: { webhooks }, ...pagination };
+ }
+ return { loading: true };
+}
+
+export function useWebhookDetails(
+ webhookId: string,
+): HttpResponse<
+ MerchantBackend.Webhooks.WebhookDetails,
+ MerchantBackend.ErrorDetail
+> {
+ const { webhookFetcher } = useBackendInstanceRequest();
+
+ const { data, error, isValidating } = useSWR<
+ HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>,
+ RequestError<MerchantBackend.ErrorDetail>
+ >([`/private/webhooks/${webhookId}`], webhookFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (isValidating) return { loading: true, data: data?.data };
+ if (data) return data;
+ if (error) return error.cause;
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/i18n/de.po b/packages/auditor-backoffice-ui/src/i18n/de.po
new file mode 100644
index 000000000..2cf0a7c1c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/de.po
@@ -0,0 +1,2742 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-12-04 13:44+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Zurück"
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/en.po b/packages/auditor-backoffice-ui/src/i18n/en.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/en.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/es.po b/packages/auditor-backoffice-ui/src/i18n/es.po
new file mode 100644
index 000000000..10ec0cf3b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/es.po
@@ -0,0 +1,2854 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-13 10:14+0000\n"
+"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr "Continuar"
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr "Limpiar"
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr "no es el mismo que el token de acceso actual"
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr "no puede ser vacío"
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr "no puede ser igual al viejo token"
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr "no son iguales"
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr "Está actualizando el token de acceso para la instancia con id %1$s"
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr "Viejo token de acceso"
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr "acceder al token en uso actualmente"
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr "Nuevo token de acceso"
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr "siguiente token de acceso a usar"
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr "Repetir token de acceso"
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr "confirmar el mismo token de acceso"
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr "Limpiar el token de acceso significa acceso público a la instancia"
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr "no puede ser igual al anterior token de acceso"
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr "Está estableciendo el token de acceso para la nueva instancia"
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+"Con el método de autorización externa no se hará ninguna revisión por el "
+"backend del comerciante"
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr "Establecer autorización externa"
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr "Establecer token de acceso"
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr "Operación en progreso..."
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr "La operación será automáticamente cancelada luego de %1$s segundos"
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr "Instancias"
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr "Eliminar"
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr "agregar nueva instancia"
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr "ID"
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr "Editar"
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr "Purgar"
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "Todavía no hay instancias, agregue más presionando el signo +"
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr "Solo mostrar instancias activas"
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr "Activo"
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr "Mostrar solo instancias eliminadas"
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr "Eliminado"
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr "Mostrar todas las instancias"
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr "Todo"
+
+#: src/paths/admin/list/index.tsx:101
+#, fuzzy, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr "Fallo al eliminar instancia"
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr "Fallo al purgar la instancia"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr "Verificación KYC pendiente"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr "Expirado"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr "Cuenta objetivo"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr "URL de KYC"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr "Código"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr "Estado http"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr "¡No hay verificación kyc pendiente!"
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr "cambiar valor a fecha desconocida"
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr "cambiar valor a vacío"
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr "limpiar"
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr "cambiar valor a nunca"
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr "nunca"
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr "País"
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr "Dirección"
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr "Número de edificio"
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr "Nombre de edificio"
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr "Calle"
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr "Código postal"
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr "Ubicación de ciudad"
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr "Ciudad"
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr "Distrito"
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr "Subdivisión de país"
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr "Id de producto"
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr "Descripcion"
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, fuzzy, c-format
+msgid "Product"
+msgstr "Productos"
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr "buscar productos por su descripción o ID"
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr "no se encontraron productos con esa descripción"
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr "Debe ingresar un identificador de producto válido."
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr "¡Cantidad debe ser mayor que 0!"
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, fuzzy, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+"Esta cantidad excede las existencias restantes. Actualmente, solo quedan "
+"%1$s unidades sin reservar en las existencias."
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr "Cantidad"
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr "cuántos productos serán agregados"
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr "Agregar del inventario"
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr "La imagen debe ser mas chica que 1 MB"
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr "Agregar"
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr "Eliminar"
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr "Ningun impuesto configurado para este producto."
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+"Impuestos pueden estar en divisas que difieren de la principal divisa usada "
+"por el comerciante."
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+"Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr "Nombre legal del impuesto, e.g. IVA o arancel."
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr "agregar impuesto a la lista de impuestos"
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr "describa y agregue un producto que no está en la lista de inventarios"
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr "Agregue un producto personalizado"
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr "Complete información del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr "Imagen"
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr "foto del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr "descripción completa del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr "Unidad"
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr "nombre de la unidad del producto"
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr "Precio"
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr "monto de la divisa actual"
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr "Impuestos"
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr "imagen"
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr "descripción"
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr "cantidad"
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr "precio unitario"
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr "precio total"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr "requerido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, fuzzy, c-format
+msgid "not valid"
+msgstr "no es un json válido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr "debe ser mayor que 0"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr "no es un json válido"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr "deberían ser en el futuro"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr "plazo de reembolso no puede ser antes que el plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de "
+"reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+"el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr "debería tener un plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr "reembolso automático no puede ser después qu el plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr "Manejar productos en orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr "Manejar lista de productos en la orden."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr "Remover este producto de la orden."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr "Precio total"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr "precio total de producto agregado"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr "Monto a ser pagado por el cliente"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr "Precio de la orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr "Precio final de la orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr "Resumen"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr "Título de la orden a ser mostrado al cliente"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr "Envío y cumplimiento"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr "Fecha de entrega"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr "Plazo para la entrega física asegurado por el comerciante."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr "Ubicación"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr "dirección a donde los productos serán entregados"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL de cumplimiento"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr "URL al cual el usuario será redirigido luego de pago exitoso."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr "Opciones de pago de Taler"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr "Sobreescribir pagos por omisión de Taler para esta orden"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Payment deadline"
+msgstr "Plazo de pago"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+"Plazo límite para que el cliente pague por la oferta antes de que expire. "
+"Productos del inventario serán reservados hasta este plazo límite."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr "Plazo de reembolso"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+"Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr "Plazo de la transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr "Plazo para que el exchange haga la transferencia."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, fuzzy, c-format
+msgid "Auto-refund deadline"
+msgstr "Plazo de reembolso automático"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+"Tiempo hasta el cual la billetera será automáticamente revisada por "
+"reembolsos win interación por parte del usuario."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr "Máxima tarifa de depósito"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+"Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para "
+"esta orden. Mayores tarifas de depósito deben ser cubiertas completamente "
+"por el consumidor."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr "Máxima tarifa de transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Amortización de comisión de transferencia"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, fuzzy, c-format
+msgid "Create token"
+msgstr "Administrar token"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, fuzzy, c-format
+msgid "Minimum age required"
+msgstr "Login necesario"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, fuzzy, c-format
+msgid "Additional information"
+msgstr "Información extra"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr "días"
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr "horas"
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr "minutos"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#: src/components/form/InputDuration.tsx:53
+#, fuzzy, c-format
+msgid "forever"
+msgstr "nunca"
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr "Órdenes"
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, fuzzy, c-format
+msgid "create order"
+msgstr "creado"
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr "copiar url"
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr "¡No se encontraron órdenes que emparejen su búsqueda!"
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr "duplicado"
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr "formato inválido"
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr "este monto excede el monto reembolsable"
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr "fecha"
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr "monto"
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr "razón"
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr "monto a ser reembolsado"
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr "Razón"
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr "Elija uno..."
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr "pedido por el consumidor"
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr "otro"
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr "por qué esta orden está siendo reembolsada"
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr "más información para dar contexto"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr "Términos de contrato"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr "descripción legible de toda la compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr "precio total de la transacción"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr "URL para esta compra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr "Máxima comisión"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr "Impuesto de transferencia máximo"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr "Creado en"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, fuzzy, c-format
+msgid "Auto-refund delay"
+msgstr "Plazo de reembolso automático"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, fuzzy, c-format
+msgid "Extra info"
+msgstr "Información extra"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr "Orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr "reclamado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, fuzzy, c-format
+msgid "claimed at"
+msgstr "reclamado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr "Cronología"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr "Detalles de pago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr "Estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr "Lista de producto"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr "pagados"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr "transferido"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr "reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, fuzzy, c-format
+msgid "refund order"
+msgstr "reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, fuzzy, c-format
+msgid "not refundable"
+msgstr "Máximo reembolzable:"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr "reembolzar"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, fuzzy, c-format
+msgid "Refund taken"
+msgstr "Reembolzado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, fuzzy, c-format
+msgid "Status URL"
+msgstr "URL de estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, fuzzy, c-format
+msgid "Refund URI"
+msgstr "Devolución"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr "impago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr "pagar en"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr "creado"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr "URL de estado de orden"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, fuzzy, c-format
+msgid "Payment URI"
+msgstr "URI de pago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+"Estado de orden desconocido. Esto es un error, por favor contacte a su "
+"administrador."
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr "reembolzo creado satisfactoriamente"
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, fuzzy, c-format
+msgid "order id"
+msgstr "ir a id de orden"
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr "Pagado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, fuzzy, c-format
+msgid "only show orders with refunds"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Reembolsado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr "No transferido"
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, fuzzy, c-format
+msgid "Enter an order id"
+msgstr "ir a id de orden"
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, fuzzy, c-format
+msgid "order not found"
+msgstr "Servidor no encontrado"
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, fuzzy, c-format
+msgid "could not get the order to refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/components/exception/AsyncButton.tsx:43
+#, fuzzy, c-format
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr "Administrar stock"
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr "Inifinito"
+
+#: src/components/form/InputStock.tsx:136
+#, fuzzy, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr "Ingresando"
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr "Perdido"
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr "Actual"
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr "sin stock"
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr "Próximo reabastecimiento"
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr "Dirección de entrega"
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr "Existencias"
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr "no se pudo crear el producto"
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr "Productos"
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr "Venta"
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr "Ganancia"
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr "Vendido"
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr "Gratis"
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, fuzzy, c-format
+msgid "go to product update page"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr "Actualizar"
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, fuzzy, c-format
+msgid "new price for the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, fuzzy, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, fuzzy, c-format
+msgid "Product id:"
+msgstr "Id de producto"
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, fuzzy, c-format
+msgid "it should be greater than 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, fuzzy, c-format
+msgid "Initial balance"
+msgstr "Instancia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr "URL del Exchange"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, fuzzy, c-format
+msgid "method to use for wire transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, fuzzy, c-format
+msgid "could not create reserve"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr "Válido hasta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, fuzzy, c-format
+msgid "Created balance"
+msgstr "creado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, fuzzy, c-format
+msgid "Exchange balance"
+msgstr "Monto inicial"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, fuzzy, c-format
+msgid "Committed"
+msgstr "Monto confirmado"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr "Propinas"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, fuzzy, c-format
+msgid "Authorized"
+msgstr "Token de autorización"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, fuzzy, c-format
+msgid "Expiration"
+msgstr "Información extra"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, fuzzy, c-format
+msgid "amount of tip"
+msgstr "monto"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, fuzzy, c-format
+msgid "Justification"
+msgstr "Jurisdicción"
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, fuzzy, c-format
+msgid "Reserves not yet funded"
+msgstr "Servidor no encontrado"
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, fuzzy, c-format
+msgid "add new reserve"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, fuzzy, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, fuzzy, c-format
+msgid "Expected Balance"
+msgstr "Ejecutado en"
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not create the tip"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, fuzzy, c-format
+msgid "should not be empty"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, fuzzy, c-format
+msgid "should be greater that 0"
+msgstr "Debe ser mayor a 0"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, fuzzy, c-format
+msgid "can't be empty"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, fuzzy, c-format
+msgid "Fixed summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, fuzzy, c-format
+msgid "Fixed price"
+msgstr "precio unitario"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr "Edad mínima"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, fuzzy, c-format
+msgid "Payment timeout"
+msgstr "Opciones de pago"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, fuzzy, c-format
+msgid "could not inform template"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, fuzzy, c-format
+msgid "Amount is required"
+msgstr "Login necesario"
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, fuzzy, c-format
+msgid "New order for template"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, fuzzy, c-format
+msgid "Order summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, fuzzy, c-format
+msgid "could not create order from template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, fuzzy, c-format
+msgid "Fixed amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, fuzzy, c-format
+msgid "Default amount"
+msgstr "Monto reembolzado"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, fuzzy, c-format
+msgid "Default summary"
+msgstr "Estado de orden"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, fuzzy, c-format
+msgid "load newer templates"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, fuzzy, c-format
+msgid "create qr code for the template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, fuzzy, c-format
+msgid "load older templates"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, fuzzy, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, fuzzy, c-format
+msgid "template delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, fuzzy, c-format
+msgid "could not delete the template"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, fuzzy, c-format
+msgid "could not update template"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, fuzzy, c-format
+msgid "should be one of '%1$s'"
+msgstr "deberían ser iguales"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, fuzzy, c-format
+msgid "load newer webhooks"
+msgstr "cargar nuevas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, fuzzy, c-format
+msgid "load older webhooks"
+msgstr "cargar viejas ordenes"
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, fuzzy, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, fuzzy, c-format
+msgid "webhook delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, fuzzy, c-format
+msgid "could not delete the webhook"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, fuzzy, c-format
+msgid "check the id, does not look valid"
+msgstr "verificar el id, no parece válido"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr "debería tener 52 caracteres, actualmente %1$s"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr "La URL no tiene el formato correcto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, fuzzy, c-format
+msgid "Wire transfer ID"
+msgstr "Id de transferencia"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr "Transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, fuzzy, c-format
+msgid "add new transfer"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr "Crédito"
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr "Confirmado"
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr "Verificado"
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr "Ejecutado en"
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr "si"
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr "no"
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr "desconocido"
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr "eliminar transferencia seleccionada de la base de datos"
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr "cargue más transferencia luego de la última"
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr "cargar viejas transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, fuzzy, c-format
+msgid "filter by account address"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, fuzzy, c-format
+msgid "Unverified"
+msgstr "Verificado"
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, fuzzy, c-format
+msgid "is not a number"
+msgstr "Número de edificio"
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr "debe ser 1 o mayor"
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr "máximo 7 líneas"
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr "cambiar configuración de autorización"
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr "Necesita completar campos marcados y escoger un método de autorización"
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr "Esta no es una dirección de bitcoin válida."
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr "Esta no es una dirección de Ethereum válida."
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Números IBAN usualmente tienen más de 4 dígitos"
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Número IBAN usualmente tienen menos de 34 dígitos"
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código IBAN de país no encontrado"
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "Número IBAN no es válido, la suma de verificación es incorrecta"
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr "Tipo objetivo"
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr "Método a usar para la transferencia"
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr "Enrutamiento"
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr "Número de enrutamiento."
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr "Cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, fuzzy, c-format
+msgid "Account number."
+msgstr "Dirección de cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr "Interfaz de pago unificado."
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, fuzzy, c-format
+msgid "Business name"
+msgstr "Nombre de edificio"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr "URL de sitio web"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr "Cuenta bancaria"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr "Impuesto máximo de deposito por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr "Impuesto máximo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr "Amortización de impuesto de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr "Jurisdicción"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr "Jurisdicción para disputas legales con el comerciante."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, fuzzy, c-format
+msgid "Default payment delay"
+msgstr "Retrazo de pago por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr "Retrazo de transferencia por omisión"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr "ID de instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, fuzzy, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+"Limpiar el token de autorización significa acceso público a la instancia"
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr "Administrar token de acceso"
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr "Fallo al crear la instancia"
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr "Login necesario"
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, fuzzy, c-format
+msgid "Access Token"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, fuzzy, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr "Servidir reporto un problema: HTTP status #%1$s"
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr "Acceso denegado"
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, fuzzy, c-format
+msgid "No 'default' instance configured yet."
+msgstr "Sin instancia default"
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr "Instancia"
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Configuración"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr "Conexión"
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr "Nuevo"
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr "Lista"
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr "Salir"
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr "Verifica que el token sea valido"
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr "No se pudo acceder al servidor."
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr "No se pudo inferir el id de la instancia con la url %1$s"
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr "Servidor no encontrado"
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr "Error inesperado"
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr "El valor %1$s es invalido para una URL de pago"
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr "agregar elemento a la lista"
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr "Agregar"
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr "Borrando"
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr "Cambiando"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr "ID de pedido"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr "URL de pago"
+
+#, c-format
+#~ msgid "Couldn't access the server"
+#~ msgstr "No se pudo aceder al servidor"
+
+#, c-format
+#~ msgid "HTTP status #%1$s: Server reported a problem"
+#~ msgstr "HTTP status #%1$s: Servidor reporto un problema"
+
+#, c-format
+#~ msgid "Got message: \"%1$s\" from: %2$s"
+#~ msgstr "Recibimos el mensaje: %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "in order to use merchant backoffice, you should create the default "
+#~ "instance"
+#~ msgstr ""
+#~ "para usar el merchant backoffice, debería crear la instancia default"
+
+#, c-format
+#~ msgid "Got message: %1$s from: %2$s"
+#~ msgstr "Recibimos el mensaje %1$s desde %2$s"
+
+#, c-format
+#~ msgid ""
+#~ "Please enter your auth token. Token should have \"secret-token:\" and "
+#~ "start with Bearer or ApiKey"
+#~ msgstr ""
+#~ "Por favor ingrese su token de autorización. El token debe tener \"secret-"
+#~ "token\" y comenzar con Bearer o ApiKey"
+
+#, c-format
+#~ msgid "pick a date"
+#~ msgstr "elegir una fecha"
+
+#, c-format
+#~ msgid "no results"
+#~ msgstr "Sin resultados"
+
+#, c-format
+#~ msgid "current stock will change from %1$s to %2$s"
+#~ msgstr "stock actual cambiará desde %1$s a %2$s"
+
+#, c-format
+#~ msgid "current stock will stay at %1$s"
+#~ msgstr "stock actual seguirá en %1$s"
+
+#, c-format
+#~ msgid "this product has no taxes"
+#~ msgstr "este producto no tiene impuestos"
+
+#, c-format
+#~ msgid "Inventory products"
+#~ msgstr "Productos de inventario"
+
+#, c-format
+#~ msgid "Total tax"
+#~ msgstr "Impuesto total"
+
+#, c-format
+#~ msgid "Net"
+#~ msgstr "Neto"
+
+#, c-format
+#~ msgid "select a product first"
+#~ msgstr "seleccione un producto primero"
+
+#, c-format
+#~ msgid ""
+#~ "cannot be greater than current stock and quantity previously added. max: "
+#~ "%1$s"
+#~ msgstr ""
+#~ "no puede ser mayor al stock actual y la cantidad previamente agregada. "
+#~ "máximo: %1$s"
+
+#, c-format
+#~ msgid "cannot be greater than current stock %1$s"
+#~ msgstr "no puede ser mayor al stock actual %1$s"
+
+#, c-format
+#~ msgid "Deposit total"
+#~ msgstr "Total depositado"
+
+#, c-format
+#~ msgid "Merchant initial amount"
+#~ msgstr "Monto inicial"
+
+#, c-format
+#~ msgid "Account Address"
+#~ msgstr "Dirección de cuenta"
diff --git a/packages/auditor-backoffice-ui/src/i18n/fr.po b/packages/auditor-backoffice-ui/src/i18n/fr.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/fr.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/it.po b/packages/auditor-backoffice-ui/src/i18n/it.po
new file mode 100644
index 000000000..4055af10e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/it.po
@@ -0,0 +1,2742 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-16 12:43+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr "Importo"
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr "Indietro"
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr "Rimborsato"
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/poheader b/packages/auditor-backoffice-ui/src/i18n/poheader
new file mode 100644
index 000000000..7ddcf49b8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/poheader
@@ -0,0 +1,27 @@
+# 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/>
+
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/auditor-backoffice-ui/src/i18n/strings-prelude b/packages/auditor-backoffice-ui/src/i18n/strings-prelude
new file mode 100644
index 000000000..6c68662de
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/strings-prelude
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
diff --git a/packages/auditor-backoffice-ui/src/i18n/strings.ts b/packages/auditor-backoffice-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..65dc41358
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/strings.ts
@@ -0,0 +1,9655 @@
+/*
+ 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/>
+ */
+
+/*eslint quote-props: ["error", "consistent"]*/
+export const strings: {[s: string]: any} = {};
+
+strings['de'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['en'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['es'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "es"
+ },
+ "Cancel": [
+ "Cancelar"
+ ],
+ "%1$s": [
+ "%1$s"
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ "Continuar"
+ ],
+ "Clear": [
+ "Limpiar"
+ ],
+ "Confirm": [
+ "Confirmar"
+ ],
+ "is not the same as the current access token": [
+ "no es el mismo que el token de acceso actual"
+ ],
+ "cannot be empty": [
+ "no puede ser vacío"
+ ],
+ "cannot be the same as the old token": [
+ "no puede ser igual al viejo token"
+ ],
+ "is not the same": [
+ "no son iguales"
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ "Está actualizando el token de acceso para la instancia con id %1$s"
+ ],
+ "Old access token": [
+ "Viejo token de acceso"
+ ],
+ "access token currently in use": [
+ "acceder al token en uso actualmente"
+ ],
+ "New access token": [
+ "Nuevo token de acceso"
+ ],
+ "next access token to be used": [
+ "siguiente token de acceso a usar"
+ ],
+ "Repeat access token": [
+ "Repetir token de acceso"
+ ],
+ "confirm the same access token": [
+ "confirmar el mismo token de acceso"
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ "Limpiar el token de acceso significa acceso público a la instancia"
+ ],
+ "cannot be the same as the old access token": [
+ "no puede ser igual al anterior token de acceso"
+ ],
+ "You are setting the access token for the new instance": [
+ "Está estableciendo el token de acceso para la nueva instancia"
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ "Con el método de autorización externa no se hará ninguna revisión por el backend del comerciante"
+ ],
+ "Set external authorization": [
+ "Establecer autorización externa"
+ ],
+ "Set access token": [
+ "Establecer token de acceso"
+ ],
+ "Operation in progress...": [
+ "Operación en progreso..."
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ "La operación será automáticamente cancelada luego de %1$s segundos"
+ ],
+ "Instances": [
+ "Instancias"
+ ],
+ "Delete": [
+ "Eliminar"
+ ],
+ "add new instance": [
+ "agregar nueva instancia"
+ ],
+ "ID": [
+ "ID"
+ ],
+ "Name": [
+ "Nombre"
+ ],
+ "Edit": [
+ "Editar"
+ ],
+ "Purge": [
+ "Purgar"
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ "Todavía no hay instancias, agregue más presionando el signo +"
+ ],
+ "Only show active instances": [
+ "Solo mostrar instancias activas"
+ ],
+ "Active": [
+ "Activo"
+ ],
+ "Only show deleted instances": [
+ "Mostrar solo instancias eliminadas"
+ ],
+ "Deleted": [
+ "Eliminado"
+ ],
+ "Show all instances": [
+ "Mostrar todas las instancias"
+ ],
+ "All": [
+ "Todo"
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ "La instancia '%1$s' (ID: %2$s) fue eliminada"
+ ],
+ "Failed to delete instance": [
+ "Fallo al eliminar instancia"
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
+ ],
+ "Failed to purge instance": [
+ "Fallo al purgar la instancia"
+ ],
+ "Pending KYC verification": [
+ "Verificación KYC pendiente"
+ ],
+ "Timed out": [
+ "Expirado"
+ ],
+ "Exchange": [
+ "Exchange"
+ ],
+ "Target account": [
+ "Cuenta objetivo"
+ ],
+ "KYC URL": [
+ "URL de KYC"
+ ],
+ "Code": [
+ "Código"
+ ],
+ "Http Status": [
+ "Estado http"
+ ],
+ "No pending kyc verification!": [
+ "¡No hay verificación kyc pendiente!"
+ ],
+ "change value to unknown date": [
+ "cambiar valor a fecha desconocida"
+ ],
+ "change value to empty": [
+ "cambiar valor a vacío"
+ ],
+ "clear": [
+ "limpiar"
+ ],
+ "change value to never": [
+ "cambiar valor a nunca"
+ ],
+ "never": [
+ "nunca"
+ ],
+ "Country": [
+ "País"
+ ],
+ "Address": [
+ "Dirección"
+ ],
+ "Building number": [
+ "Número de edificio"
+ ],
+ "Building name": [
+ "Nombre de edificio"
+ ],
+ "Street": [
+ "Calle"
+ ],
+ "Post code": [
+ "Código postal"
+ ],
+ "Town location": [
+ "Ubicación de ciudad"
+ ],
+ "Town": [
+ "Ciudad"
+ ],
+ "District": [
+ "Distrito"
+ ],
+ "Country subdivision": [
+ "Subdivisión de país"
+ ],
+ "Product id": [
+ "Id de producto"
+ ],
+ "Description": [
+ "Descripcion"
+ ],
+ "Product": [
+ "Productos"
+ ],
+ "search products by it's description or id": [
+ "buscar productos por su descripción o ID"
+ ],
+ "no products found with that description": [
+ "no se encontraron productos con esa descripción"
+ ],
+ "You must enter a valid product identifier.": [
+ "Debe ingresar un identificador de producto válido."
+ ],
+ "Quantity must be greater than 0!": [
+ "¡Cantidad debe ser mayor que 0!"
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ "Esta cantidad excede las existencias restantes. Actualmente, solo quedan %1$s unidades sin reservar en las existencias."
+ ],
+ "Quantity": [
+ "Cantidad"
+ ],
+ "how many products will be added": [
+ "cuántos productos serán agregados"
+ ],
+ "Add from inventory": [
+ "Agregar del inventario"
+ ],
+ "Image should be smaller than 1 MB": [
+ "La imagen debe ser mas chica que 1 MB"
+ ],
+ "Add": [
+ "Agregar"
+ ],
+ "Remove": [
+ "Eliminar"
+ ],
+ "No taxes configured for this product.": [
+ "Ningun impuesto configurado para este producto."
+ ],
+ "Amount": [
+ "Monto"
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ "Impuestos pueden estar en divisas que difieren de la principal divisa usada por el comerciante."
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ "Ingrese divisa y valor separado por dos puntos, e.g. &quot;USD:2.3&quot;."
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ "Nombre legal del impuesto, e.g. IVA o arancel."
+ ],
+ "add tax to the tax list": [
+ "agregar impuesto a la lista de impuestos"
+ ],
+ "describe and add a product that is not in the inventory list": [
+ "describa y agregue un producto que no está en la lista de inventarios"
+ ],
+ "Add custom product": [
+ "Agregue un producto personalizado"
+ ],
+ "Complete information of the product": [
+ "Complete información del producto"
+ ],
+ "Image": [
+ "Imagen"
+ ],
+ "photo of the product": [
+ "foto del producto"
+ ],
+ "full product description": [
+ "descripción completa del producto"
+ ],
+ "Unit": [
+ "Unidad"
+ ],
+ "name of the product unit": [
+ "nombre de la unidad del producto"
+ ],
+ "Price": [
+ "Precio"
+ ],
+ "amount in the current currency": [
+ "monto de la divisa actual"
+ ],
+ "Taxes": [
+ "Impuestos"
+ ],
+ "image": [
+ "imagen"
+ ],
+ "description": [
+ "descripción"
+ ],
+ "quantity": [
+ "cantidad"
+ ],
+ "unit price": [
+ "precio unitario"
+ ],
+ "total price": [
+ "precio total"
+ ],
+ "required": [
+ "requerido"
+ ],
+ "not valid": [
+ "no es un json válido"
+ ],
+ "must be greater than 0": [
+ "debe ser mayor que 0"
+ ],
+ "not a valid json": [
+ "no es un json válido"
+ ],
+ "should be in the future": [
+ "deberían ser en el futuro"
+ ],
+ "refund deadline cannot be before pay deadline": [
+ "plazo de reembolso no puede ser antes que el plazo de pago"
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de reembolso"
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ "el plazo de la transferencia bancaria no puede ser antes que el plazo de pago"
+ ],
+ "should have a refund deadline": [
+ "debería tener un plazo de reembolso"
+ ],
+ "auto refund cannot be after refund deadline": [
+ "reembolso automático no puede ser después qu el plazo de reembolso"
+ ],
+ "Manage products in order": [
+ "Manejar productos en orden"
+ ],
+ "Manage list of products in the order.": [
+ "Manejar lista de productos en la orden."
+ ],
+ "Remove this product from the order.": [
+ "Remover este producto de la orden."
+ ],
+ "Total price": [
+ "Precio total"
+ ],
+ "total product price added up": [
+ "precio total de producto agregado"
+ ],
+ "Amount to be paid by the customer": [
+ "Monto a ser pagado por el cliente"
+ ],
+ "Order price": [
+ "Precio de la orden"
+ ],
+ "final order price": [
+ "Precio final de la orden"
+ ],
+ "Summary": [
+ "Resumen"
+ ],
+ "Title of the order to be shown to the customer": [
+ "Título de la orden a ser mostrado al cliente"
+ ],
+ "Shipping and Fulfillment": [
+ "Envío y cumplimiento"
+ ],
+ "Delivery date": [
+ "Fecha de entrega"
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ "Plazo para la entrega física asegurado por el comerciante."
+ ],
+ "Location": [
+ "Ubicación"
+ ],
+ "address where the products will be delivered": [
+ "dirección a donde los productos serán entregados"
+ ],
+ "Fulfillment URL": [
+ "URL de cumplimiento"
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ "URL al cual el usuario será redirigido luego de pago exitoso."
+ ],
+ "Taler payment options": [
+ "Opciones de pago de Taler"
+ ],
+ "Override default Taler payment settings for this order": [
+ "Sobreescribir pagos por omisión de Taler para esta orden"
+ ],
+ "Payment deadline": [
+ "Plazo de pago"
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ "Plazo límite para que el cliente pague por la oferta antes de que expire. Productos del inventario serán reservados hasta este plazo límite."
+ ],
+ "Refund deadline": [
+ "Plazo de reembolso"
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ "Tiempo hasta el cual la orden puede ser reembolsada por el comerciante."
+ ],
+ "Wire transfer deadline": [
+ "Plazo de la transferencia"
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ "Plazo para que el exchange haga la transferencia."
+ ],
+ "Auto-refund deadline": [
+ "Plazo de reembolso automático"
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ "Tiempo hasta el cual la billetera será automáticamente revisada por reembolsos win interación por parte del usuario."
+ ],
+ "Maximum deposit fee": [
+ "Máxima tarifa de depósito"
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ "Máxima tarifa de depósito que el comerciante esta dispuesto a cubir para esta orden. Mayores tarifas de depósito deben ser cubiertas completamente por el consumidor."
+ ],
+ "Maximum wire fee": [
+ "Máxima tarifa de transferencia"
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ "Amortización de comisión de transferencia"
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ "Administrar token"
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ "Login necesario"
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ "Información extra"
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ "días"
+ ],
+ "hours": [
+ "horas"
+ ],
+ "minutes": [
+ "minutos"
+ ],
+ "seconds": [
+ "segundos"
+ ],
+ "forever": [
+ "nunca"
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ "Órdenes"
+ ],
+ "create order": [
+ "creado"
+ ],
+ "load newer orders": [
+ "cargar nuevas ordenes"
+ ],
+ "Date": [
+ "Fecha"
+ ],
+ "Refund": [
+ "Devolución"
+ ],
+ "copy url": [
+ "copiar url"
+ ],
+ "load older orders": [
+ "cargar viejas ordenes"
+ ],
+ "No orders have been found matching your query!": [
+ "¡No se encontraron órdenes que emparejen su búsqueda!"
+ ],
+ "duplicated": [
+ "duplicado"
+ ],
+ "invalid format": [
+ "formato inválido"
+ ],
+ "this value exceed the refundable amount": [
+ "este monto excede el monto reembolsable"
+ ],
+ "date": [
+ "fecha"
+ ],
+ "amount": [
+ "monto"
+ ],
+ "reason": [
+ "razón"
+ ],
+ "amount to be refunded": [
+ "monto a ser reembolsado"
+ ],
+ "Max refundable:": [
+ "Máximo reembolzable:"
+ ],
+ "Reason": [
+ "Razón"
+ ],
+ "Choose one...": [
+ "Elija uno..."
+ ],
+ "requested by the customer": [
+ "pedido por el consumidor"
+ ],
+ "other": [
+ "otro"
+ ],
+ "why this order is being refunded": [
+ "por qué esta orden está siendo reembolsada"
+ ],
+ "more information to give context": [
+ "más información para dar contexto"
+ ],
+ "Contract Terms": [
+ "Términos de contrato"
+ ],
+ "human-readable description of the whole purchase": [
+ "descripción legible de toda la compra"
+ ],
+ "total price for the transaction": [
+ "precio total de la transacción"
+ ],
+ "URL for this purchase": [
+ "URL para esta compra"
+ ],
+ "Max fee": [
+ "Máxima comisión"
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ "Impuesto de transferencia máximo"
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ "Creado en"
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ "Plazo de reembolso automático"
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ "Información extra"
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ "Orden"
+ ],
+ "claimed": [
+ "reclamado"
+ ],
+ "claimed at": [
+ "reclamado"
+ ],
+ "Timeline": [
+ "Cronología"
+ ],
+ "Payment details": [
+ "Detalles de pago"
+ ],
+ "Order status": [
+ "Estado de orden"
+ ],
+ "Product list": [
+ "Lista de producto"
+ ],
+ "paid": [
+ "pagados"
+ ],
+ "wired": [
+ "transferido"
+ ],
+ "refunded": [
+ "reembolzado"
+ ],
+ "refund order": [
+ "reembolzado"
+ ],
+ "not refundable": [
+ "Máximo reembolzable:"
+ ],
+ "refund": [
+ "reembolzar"
+ ],
+ "Refunded amount": [
+ "Monto reembolzado"
+ ],
+ "Refund taken": [
+ "Reembolzado"
+ ],
+ "Status URL": [
+ "URL de estado de orden"
+ ],
+ "Refund URI": [
+ "Devolución"
+ ],
+ "unpaid": [
+ "impago"
+ ],
+ "pay at": [
+ "pagar en"
+ ],
+ "created at": [
+ "creado"
+ ],
+ "Order status URL": [
+ "URL de estado de orden"
+ ],
+ "Payment URI": [
+ "URI de pago"
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ "Estado de orden desconocido. Esto es un error, por favor contacte a su administrador."
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ "reembolzo creado satisfactoriamente"
+ ],
+ "could not create the refund": [
+ "No se pudo create el reembolso"
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ "ir a id de orden"
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ "Pagado"
+ ],
+ "only show orders with refunds": [
+ "No se pudo create el reembolso"
+ ],
+ "Refunded": [
+ "Reembolzado"
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ "No transferido"
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ "ir a id de orden"
+ ],
+ "order not found": [
+ "Servidor no encontrado"
+ ],
+ "could not get the order to refund": [
+ "No se pudo create el reembolso"
+ ],
+ "Loading...": [
+ "Cargando..."
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ "Administrar stock"
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ "Inifinito"
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ "la pérdida no puede ser mayor al stock actual + entrante (max %1$s )"
+ ],
+ "Incoming": [
+ "Ingresando"
+ ],
+ "Lost": [
+ "Perdido"
+ ],
+ "Current": [
+ "Actual"
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ "sin stock"
+ ],
+ "Next restock": [
+ "Próximo reabastecimiento"
+ ],
+ "Delivery address": [
+ "Dirección de entrega"
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ "Existencias"
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ "no se pudo crear el producto"
+ ],
+ "Products": [
+ "Productos"
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ "Venta"
+ ],
+ "Profit": [
+ "Ganancia"
+ ],
+ "Sold": [
+ "Vendido"
+ ],
+ "free": [
+ "Gratis"
+ ],
+ "go to product update page": [
+ "producto actualizado correctamente"
+ ],
+ "Update": [
+ "Actualizar"
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ "no se pudo actualizar el producto"
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "product updated successfully": [
+ "producto actualizado correctamente"
+ ],
+ "could not update the product": [
+ "no se pudo actualizar el producto"
+ ],
+ "product delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the product": [
+ "no se pudo eliminar el producto"
+ ],
+ "Product id:": [
+ "Id de producto"
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ "Debe ser mayor a 0"
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ "Instancia"
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ "URL del Exchange"
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ "Siguiente"
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ "No se pudo create el reembolso"
+ ],
+ "Valid until": [
+ "Válido hasta"
+ ],
+ "Created balance": [
+ "creado"
+ ],
+ "Exchange balance": [
+ "Monto inicial"
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ "Monto confirmado"
+ ],
+ "Account address": [
+ "Dirección de cuenta"
+ ],
+ "Subject": [
+ "Asunto"
+ ],
+ "Tips": [
+ "Propinas"
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ "Token de autorización"
+ ],
+ "Expiration": [
+ "Información extra"
+ ],
+ "amount of tip": [
+ "monto"
+ ],
+ "Justification": [
+ "Jurisdicción"
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ "Servidor no encontrado"
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ "cargar nuevas transferencias"
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ "No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "Expected Balance": [
+ "Ejecutado en"
+ ],
+ "could not create the tip": [
+ "No se pudo create el reembolso"
+ ],
+ "should not be empty": [
+ "no puede ser vacío"
+ ],
+ "should be greater that 0": [
+ "Debe ser mayor a 0"
+ ],
+ "can't be empty": [
+ "no puede ser vacío"
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ "Estado de orden"
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ "precio unitario"
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ "Edad mínima"
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ "Opciones de pago"
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ "no se pudo informar la transferencia"
+ ],
+ "Amount is required": [
+ "Login necesario"
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ "cargar viejas transferencias"
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ "Estado de orden"
+ ],
+ "could not create order from template": [
+ "No se pudo create el reembolso"
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ "Monto reembolzado"
+ ],
+ "Default amount": [
+ "Monto reembolzado"
+ ],
+ "Default summary": [
+ "Estado de orden"
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ "cargar nuevas transferencias"
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ "No se pudo create el reembolso"
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ "cargar viejas transferencias"
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "template delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the template": [
+ "no se pudo eliminar el producto"
+ ],
+ "could not update template": [
+ "no se pudo actualizar el producto"
+ ],
+ "should be one of '%1$s'": [
+ "deberían ser iguales"
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ "URL"
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ "cargar nuevas ordenes"
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ "cargar viejas ordenes"
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ "No hay propinas todavía, agregar mas presionando el signo +"
+ ],
+ "webhook delete successfully": [
+ "producto fue eliminado correctamente"
+ ],
+ "could not delete the webhook": [
+ "no se pudo eliminar el producto"
+ ],
+ "check the id, does not look valid": [
+ "verificar el id, no parece válido"
+ ],
+ "should have 52 characters, current %1$s": [
+ "debería tener 52 caracteres, actualmente %1$s"
+ ],
+ "URL doesn't have the right format": [
+ "La URL no tiene el formato correcto"
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ "Id de transferencia"
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Transfers": [
+ "Transferencias"
+ ],
+ "add new transfer": [
+ "cargar nuevas transferencias"
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ "cargar nuevas transferencias"
+ ],
+ "Credit": [
+ "Crédito"
+ ],
+ "Confirmed": [
+ "Confirmado"
+ ],
+ "Verified": [
+ "Verificado"
+ ],
+ "Executed at": [
+ "Ejecutado en"
+ ],
+ "yes": [
+ "si"
+ ],
+ "no": [
+ "no"
+ ],
+ "unknown": [
+ "desconocido"
+ ],
+ "delete selected transfer from the database": [
+ "eliminar transferencia seleccionada de la base de datos"
+ ],
+ "load more transfer after the last one": [
+ "cargue más transferencia luego de la última"
+ ],
+ "load older transfers": [
+ "cargar viejas transferencias"
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ "No hay transferencias todavía, agregar mas presionando el signo +"
+ ],
+ "filter by account address": [
+ "Dirección de cuenta"
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ "Verificado"
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ "Número de edificio"
+ ],
+ "must be 1 or greater": [
+ "debe ser 1 o mayor"
+ ],
+ "max 7 lines": [
+ "máximo 7 líneas"
+ ],
+ "change authorization configuration": [
+ "cambiar configuración de autorización"
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ "Necesita completar campos marcados y escoger un método de autorización"
+ ],
+ "This is not a valid bitcoin address.": [
+ "Esta no es una dirección de bitcoin válida."
+ ],
+ "This is not a valid Ethereum address.": [
+ "Esta no es una dirección de Ethereum válida."
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ "Números IBAN usualmente tienen más de 4 dígitos"
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Número IBAN usualmente tienen menos de 34 dígitos"
+ ],
+ "IBAN country code not found": [
+ "Código IBAN de país no encontrado"
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ "Número IBAN no es válido, la suma de verificación es incorrecta"
+ ],
+ "Target type": [
+ "Tipo objetivo"
+ ],
+ "Method to use for wire transfer": [
+ "Método a usar para la transferencia"
+ ],
+ "Routing": [
+ "Enrutamiento"
+ ],
+ "Routing number.": [
+ "Número de enrutamiento."
+ ],
+ "Account": [
+ "Cuenta"
+ ],
+ "Account number.": [
+ "Dirección de cuenta"
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ "Interfaz de pago unificado."
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ "Nombre de edificio"
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ "URL de sitio web"
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ "Cuenta bancaria"
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ "Impuesto máximo de deposito por omisión"
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ "Impuesto máximo de transferencia por omisión"
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ "Amortización de impuesto de transferencia por omisión"
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ "Jurisdicción"
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ "Jurisdicción para disputas legales con el comerciante."
+ ],
+ "Default payment delay": [
+ "Retrazo de pago por omisión"
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ "Retrazo de transferencia por omisión"
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ "ID de instancia"
+ ],
+ "Change the authorization method use for this instance.": [
+ "Limpiar el token de autorización significa acceso público a la instancia"
+ ],
+ "Manage access token": [
+ "Administrar token de acceso"
+ ],
+ "Failed to create instance": [
+ "Fallo al crear la instancia"
+ ],
+ "Login required": [
+ "Login necesario"
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ "Acceso denegado"
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ "Servidir reporto un problema: HTTP status #%1$s"
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ "Acceso denegado"
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ "Sin instancia default"
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ "Instancia"
+ ],
+ "Settings": [
+ "Configuración"
+ ],
+ "Connection": [
+ "Conexión"
+ ],
+ "New": [
+ "Nuevo"
+ ],
+ "List": [
+ "Lista"
+ ],
+ "Log out": [
+ "Salir"
+ ],
+ "Check your token is valid": [
+ "Verifica que el token sea valido"
+ ],
+ "Couldn't access the server.": [
+ "No se pudo acceder al servidor."
+ ],
+ "Could not infer instance id from url %1$s": [
+ "No se pudo inferir el id de la instancia con la url %1$s"
+ ],
+ "Server not found": [
+ "Servidor no encontrado"
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ "Recibimos el mensaje %1$s desde %2$s"
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ "Error inesperado"
+ ],
+ "The value %1$s is invalid for a payment url": [
+ "El valor %1$s es invalido para una URL de pago"
+ ],
+ "add element to the list": [
+ "agregar elemento a la lista"
+ ],
+ "add": [
+ "Agregar"
+ ],
+ "Deleting": [
+ "Borrando"
+ ],
+ "Changing": [
+ "Cambiando"
+ ],
+ "Order ID": [
+ "ID de pedido"
+ ],
+ "Payment URL": [
+ "URL de pago"
+ ]
+ }
+ }
+};
+
+strings['fr'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['it'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
+strings['sv'] = {
+ "domain": "messages",
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": ""
+ },
+ "Cancel": [
+ ""
+ ],
+ "%1$s": [
+ ""
+ ],
+ "Close": [
+ ""
+ ],
+ "Continue": [
+ ""
+ ],
+ "Clear": [
+ ""
+ ],
+ "Confirm": [
+ ""
+ ],
+ "is not the same as the current access token": [
+ ""
+ ],
+ "cannot be empty": [
+ ""
+ ],
+ "cannot be the same as the old token": [
+ ""
+ ],
+ "is not the same": [
+ ""
+ ],
+ "You are updating the access token from instance with id %1$s": [
+ ""
+ ],
+ "Old access token": [
+ ""
+ ],
+ "access token currently in use": [
+ ""
+ ],
+ "New access token": [
+ ""
+ ],
+ "next access token to be used": [
+ ""
+ ],
+ "Repeat access token": [
+ ""
+ ],
+ "confirm the same access token": [
+ ""
+ ],
+ "Clearing the access token will mean public access to the instance": [
+ ""
+ ],
+ "cannot be the same as the old access token": [
+ ""
+ ],
+ "You are setting the access token for the new instance": [
+ ""
+ ],
+ "With external authorization method no check will be done by the merchant backend": [
+ ""
+ ],
+ "Set external authorization": [
+ ""
+ ],
+ "Set access token": [
+ ""
+ ],
+ "Operation in progress...": [
+ ""
+ ],
+ "The operation will be automatically canceled after %1$s seconds": [
+ ""
+ ],
+ "Instances": [
+ ""
+ ],
+ "Delete": [
+ ""
+ ],
+ "add new instance": [
+ ""
+ ],
+ "ID": [
+ ""
+ ],
+ "Name": [
+ ""
+ ],
+ "Edit": [
+ ""
+ ],
+ "Purge": [
+ ""
+ ],
+ "There is no instances yet, add more pressing the + sign": [
+ ""
+ ],
+ "Only show active instances": [
+ ""
+ ],
+ "Active": [
+ ""
+ ],
+ "Only show deleted instances": [
+ ""
+ ],
+ "Deleted": [
+ ""
+ ],
+ "Show all instances": [
+ ""
+ ],
+ "All": [
+ ""
+ ],
+ "Instance \"%1$s\" (ID: %2$s) has been deleted": [
+ ""
+ ],
+ "Failed to delete instance": [
+ ""
+ ],
+ "Instance '%1$s' (ID: %2$s) has been disabled": [
+ ""
+ ],
+ "Failed to purge instance": [
+ ""
+ ],
+ "Pending KYC verification": [
+ ""
+ ],
+ "Timed out": [
+ ""
+ ],
+ "Exchange": [
+ ""
+ ],
+ "Target account": [
+ ""
+ ],
+ "KYC URL": [
+ ""
+ ],
+ "Code": [
+ ""
+ ],
+ "Http Status": [
+ ""
+ ],
+ "No pending kyc verification!": [
+ ""
+ ],
+ "change value to unknown date": [
+ ""
+ ],
+ "change value to empty": [
+ ""
+ ],
+ "clear": [
+ ""
+ ],
+ "change value to never": [
+ ""
+ ],
+ "never": [
+ ""
+ ],
+ "Country": [
+ ""
+ ],
+ "Address": [
+ ""
+ ],
+ "Building number": [
+ ""
+ ],
+ "Building name": [
+ ""
+ ],
+ "Street": [
+ ""
+ ],
+ "Post code": [
+ ""
+ ],
+ "Town location": [
+ ""
+ ],
+ "Town": [
+ ""
+ ],
+ "District": [
+ ""
+ ],
+ "Country subdivision": [
+ ""
+ ],
+ "Product id": [
+ ""
+ ],
+ "Description": [
+ ""
+ ],
+ "Product": [
+ ""
+ ],
+ "search products by it's description or id": [
+ ""
+ ],
+ "no products found with that description": [
+ ""
+ ],
+ "You must enter a valid product identifier.": [
+ ""
+ ],
+ "Quantity must be greater than 0!": [
+ ""
+ ],
+ "This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
+ ""
+ ],
+ "Quantity": [
+ ""
+ ],
+ "how many products will be added": [
+ ""
+ ],
+ "Add from inventory": [
+ ""
+ ],
+ "Image should be smaller than 1 MB": [
+ ""
+ ],
+ "Add": [
+ ""
+ ],
+ "Remove": [
+ ""
+ ],
+ "No taxes configured for this product.": [
+ ""
+ ],
+ "Amount": [
+ ""
+ ],
+ "Taxes can be in currencies that differ from the main currency used by the merchant.": [
+ ""
+ ],
+ "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;.": [
+ ""
+ ],
+ "Legal name of the tax, e.g. VAT or import duties.": [
+ ""
+ ],
+ "add tax to the tax list": [
+ ""
+ ],
+ "describe and add a product that is not in the inventory list": [
+ ""
+ ],
+ "Add custom product": [
+ ""
+ ],
+ "Complete information of the product": [
+ ""
+ ],
+ "Image": [
+ ""
+ ],
+ "photo of the product": [
+ ""
+ ],
+ "full product description": [
+ ""
+ ],
+ "Unit": [
+ ""
+ ],
+ "name of the product unit": [
+ ""
+ ],
+ "Price": [
+ ""
+ ],
+ "amount in the current currency": [
+ ""
+ ],
+ "Taxes": [
+ ""
+ ],
+ "image": [
+ ""
+ ],
+ "description": [
+ ""
+ ],
+ "quantity": [
+ ""
+ ],
+ "unit price": [
+ ""
+ ],
+ "total price": [
+ ""
+ ],
+ "required": [
+ ""
+ ],
+ "not valid": [
+ ""
+ ],
+ "must be greater than 0": [
+ ""
+ ],
+ "not a valid json": [
+ ""
+ ],
+ "should be in the future": [
+ ""
+ ],
+ "refund deadline cannot be before pay deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before refund deadline": [
+ ""
+ ],
+ "wire transfer deadline cannot be before pay deadline": [
+ ""
+ ],
+ "should have a refund deadline": [
+ ""
+ ],
+ "auto refund cannot be after refund deadline": [
+ ""
+ ],
+ "Manage products in order": [
+ ""
+ ],
+ "Manage list of products in the order.": [
+ ""
+ ],
+ "Remove this product from the order.": [
+ ""
+ ],
+ "Total price": [
+ ""
+ ],
+ "total product price added up": [
+ ""
+ ],
+ "Amount to be paid by the customer": [
+ ""
+ ],
+ "Order price": [
+ ""
+ ],
+ "final order price": [
+ ""
+ ],
+ "Summary": [
+ ""
+ ],
+ "Title of the order to be shown to the customer": [
+ ""
+ ],
+ "Shipping and Fulfillment": [
+ ""
+ ],
+ "Delivery date": [
+ ""
+ ],
+ "Deadline for physical delivery assured by the merchant.": [
+ ""
+ ],
+ "Location": [
+ ""
+ ],
+ "address where the products will be delivered": [
+ ""
+ ],
+ "Fulfillment URL": [
+ ""
+ ],
+ "URL to which the user will be redirected after successful payment.": [
+ ""
+ ],
+ "Taler payment options": [
+ ""
+ ],
+ "Override default Taler payment settings for this order": [
+ ""
+ ],
+ "Payment deadline": [
+ ""
+ ],
+ "Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
+ ""
+ ],
+ "Refund deadline": [
+ ""
+ ],
+ "Time until which the order can be refunded by the merchant.": [
+ ""
+ ],
+ "Wire transfer deadline": [
+ ""
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ ""
+ ],
+ "Auto-refund deadline": [
+ ""
+ ],
+ "Time until which the wallet will automatically check for refunds without user interaction.": [
+ ""
+ ],
+ "Maximum deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.": [
+ ""
+ ],
+ "Maximum wire fee": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Wire fee amortization": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Create token": [
+ ""
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ ""
+ ],
+ "Minimum age required": [
+ ""
+ ],
+ "Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products": [
+ ""
+ ],
+ "Min age defined by the producs is %1$s": [
+ ""
+ ],
+ "Additional information": [
+ ""
+ ],
+ "Custom information to be included in the contract for this order.": [
+ ""
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ ""
+ ],
+ "days": [
+ ""
+ ],
+ "hours": [
+ ""
+ ],
+ "minutes": [
+ ""
+ ],
+ "seconds": [
+ ""
+ ],
+ "forever": [
+ ""
+ ],
+ "%1$sM": [
+ ""
+ ],
+ "%1$sY": [
+ ""
+ ],
+ "%1$sd": [
+ ""
+ ],
+ "%1$sh": [
+ ""
+ ],
+ "%1$smin": [
+ ""
+ ],
+ "%1$ssec": [
+ ""
+ ],
+ "Orders": [
+ ""
+ ],
+ "create order": [
+ ""
+ ],
+ "load newer orders": [
+ ""
+ ],
+ "Date": [
+ ""
+ ],
+ "Refund": [
+ ""
+ ],
+ "copy url": [
+ ""
+ ],
+ "load older orders": [
+ ""
+ ],
+ "No orders have been found matching your query!": [
+ ""
+ ],
+ "duplicated": [
+ ""
+ ],
+ "invalid format": [
+ ""
+ ],
+ "this value exceed the refundable amount": [
+ ""
+ ],
+ "date": [
+ ""
+ ],
+ "amount": [
+ ""
+ ],
+ "reason": [
+ ""
+ ],
+ "amount to be refunded": [
+ ""
+ ],
+ "Max refundable:": [
+ ""
+ ],
+ "Reason": [
+ ""
+ ],
+ "Choose one...": [
+ ""
+ ],
+ "requested by the customer": [
+ ""
+ ],
+ "other": [
+ ""
+ ],
+ "why this order is being refunded": [
+ ""
+ ],
+ "more information to give context": [
+ ""
+ ],
+ "Contract Terms": [
+ ""
+ ],
+ "human-readable description of the whole purchase": [
+ ""
+ ],
+ "total price for the transaction": [
+ ""
+ ],
+ "URL for this purchase": [
+ ""
+ ],
+ "Max fee": [
+ ""
+ ],
+ "maximum total deposit fee accepted by the merchant for this contract": [
+ ""
+ ],
+ "Max wire fee": [
+ ""
+ ],
+ "maximum wire fee accepted by the merchant": [
+ ""
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ ""
+ ],
+ "Created at": [
+ ""
+ ],
+ "time when this contract was generated": [
+ ""
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ ""
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ ""
+ ],
+ "transfer deadline for the exchange": [
+ ""
+ ],
+ "time indicating when the order should be delivered": [
+ ""
+ ],
+ "where the order will be delivered": [
+ ""
+ ],
+ "Auto-refund delay": [
+ ""
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ ""
+ ],
+ "Extra info": [
+ ""
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ ""
+ ],
+ "Order": [
+ ""
+ ],
+ "claimed": [
+ ""
+ ],
+ "claimed at": [
+ ""
+ ],
+ "Timeline": [
+ ""
+ ],
+ "Payment details": [
+ ""
+ ],
+ "Order status": [
+ ""
+ ],
+ "Product list": [
+ ""
+ ],
+ "paid": [
+ ""
+ ],
+ "wired": [
+ ""
+ ],
+ "refunded": [
+ ""
+ ],
+ "refund order": [
+ ""
+ ],
+ "not refundable": [
+ ""
+ ],
+ "refund": [
+ ""
+ ],
+ "Refunded amount": [
+ ""
+ ],
+ "Refund taken": [
+ ""
+ ],
+ "Status URL": [
+ ""
+ ],
+ "Refund URI": [
+ ""
+ ],
+ "unpaid": [
+ ""
+ ],
+ "pay at": [
+ ""
+ ],
+ "created at": [
+ ""
+ ],
+ "Order status URL": [
+ ""
+ ],
+ "Payment URI": [
+ ""
+ ],
+ "Unknown order status. This is an error, please contact the administrator.": [
+ ""
+ ],
+ "Back": [
+ ""
+ ],
+ "refund created successfully": [
+ ""
+ ],
+ "could not create the refund": [
+ ""
+ ],
+ "select date to show nearby orders": [
+ ""
+ ],
+ "order id": [
+ ""
+ ],
+ "jump to order with the given order ID": [
+ ""
+ ],
+ "remove all filters": [
+ ""
+ ],
+ "only show paid orders": [
+ ""
+ ],
+ "Paid": [
+ ""
+ ],
+ "only show orders with refunds": [
+ ""
+ ],
+ "Refunded": [
+ ""
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ ""
+ ],
+ "Not wired": [
+ ""
+ ],
+ "clear date filter": [
+ ""
+ ],
+ "date (YYYY/MM/DD)": [
+ ""
+ ],
+ "Enter an order id": [
+ ""
+ ],
+ "order not found": [
+ ""
+ ],
+ "could not get the order to refund": [
+ ""
+ ],
+ "Loading...": [
+ ""
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ ""
+ ],
+ "Manage stock": [
+ ""
+ ],
+ "this product has been configured without stock control": [
+ ""
+ ],
+ "Infinite": [
+ ""
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ ""
+ ],
+ "Incoming": [
+ ""
+ ],
+ "Lost": [
+ ""
+ ],
+ "Current": [
+ ""
+ ],
+ "remove stock control for this product": [
+ ""
+ ],
+ "without stock": [
+ ""
+ ],
+ "Next restock": [
+ ""
+ ],
+ "Delivery address": [
+ ""
+ ],
+ "product identification to use in URLs (for internal use only)": [
+ ""
+ ],
+ "illustration of the product for customers": [
+ ""
+ ],
+ "product description for customers": [
+ ""
+ ],
+ "Age restricted": [
+ ""
+ ],
+ "is this product restricted for customer below certain age?": [
+ ""
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ ""
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ ""
+ ],
+ "Stock": [
+ ""
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ ""
+ ],
+ "taxes included in the product price, exposed to customers": [
+ ""
+ ],
+ "Need to complete marked fields": [
+ ""
+ ],
+ "could not create product": [
+ ""
+ ],
+ "Products": [
+ ""
+ ],
+ "add product to inventory": [
+ ""
+ ],
+ "Sell": [
+ ""
+ ],
+ "Profit": [
+ ""
+ ],
+ "Sold": [
+ ""
+ ],
+ "free": [
+ ""
+ ],
+ "go to product update page": [
+ ""
+ ],
+ "Update": [
+ ""
+ ],
+ "remove this product from the database": [
+ ""
+ ],
+ "update the product with new price": [
+ ""
+ ],
+ "update product with new price": [
+ ""
+ ],
+ "add more elements to the inventory": [
+ ""
+ ],
+ "report elements lost in the inventory": [
+ ""
+ ],
+ "new price for the product": [
+ ""
+ ],
+ "the are value with errors": [
+ ""
+ ],
+ "update product with new stock and price": [
+ ""
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ ""
+ ],
+ "product updated successfully": [
+ ""
+ ],
+ "could not update the product": [
+ ""
+ ],
+ "product delete successfully": [
+ ""
+ ],
+ "could not delete the product": [
+ ""
+ ],
+ "Product id:": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ ""
+ ],
+ "it should be greater than 0": [
+ ""
+ ],
+ "must be a valid URL": [
+ ""
+ ],
+ "Initial balance": [
+ ""
+ ],
+ "balance prior to deposit": [
+ ""
+ ],
+ "Exchange URL": [
+ ""
+ ],
+ "URL of exchange": [
+ ""
+ ],
+ "Next": [
+ ""
+ ],
+ "Wire method": [
+ ""
+ ],
+ "method to use for wire transfer": [
+ ""
+ ],
+ "Select one wire method": [
+ ""
+ ],
+ "could not create reserve": [
+ ""
+ ],
+ "Valid until": [
+ ""
+ ],
+ "Created balance": [
+ ""
+ ],
+ "Exchange balance": [
+ ""
+ ],
+ "Picked up": [
+ ""
+ ],
+ "Committed": [
+ ""
+ ],
+ "Account address": [
+ ""
+ ],
+ "Subject": [
+ ""
+ ],
+ "Tips": [
+ ""
+ ],
+ "No tips has been authorized from this reserve": [
+ ""
+ ],
+ "Authorized": [
+ ""
+ ],
+ "Expiration": [
+ ""
+ ],
+ "amount of tip": [
+ ""
+ ],
+ "Justification": [
+ ""
+ ],
+ "reason for the tip": [
+ ""
+ ],
+ "URL after tip": [
+ ""
+ ],
+ "URL to visit after tip payment": [
+ ""
+ ],
+ "Reserves not yet funded": [
+ ""
+ ],
+ "Reserves ready": [
+ ""
+ ],
+ "add new reserve": [
+ ""
+ ],
+ "Expires at": [
+ ""
+ ],
+ "Initial": [
+ ""
+ ],
+ "delete selected reserve from the database": [
+ ""
+ ],
+ "authorize new tip from selected reserve": [
+ ""
+ ],
+ "There is no ready reserves yet, add more pressing the + sign or fund them": [
+ ""
+ ],
+ "Expected Balance": [
+ ""
+ ],
+ "could not create the tip": [
+ ""
+ ],
+ "should not be empty": [
+ ""
+ ],
+ "should be greater that 0": [
+ ""
+ ],
+ "can't be empty": [
+ ""
+ ],
+ "to short": [
+ ""
+ ],
+ "just letters and numbers from 2 to 7": [
+ ""
+ ],
+ "size of the key should be 32": [
+ ""
+ ],
+ "Identifier": [
+ ""
+ ],
+ "Name of the template in URLs.": [
+ ""
+ ],
+ "Describe what this template stands for": [
+ ""
+ ],
+ "Fixed summary": [
+ ""
+ ],
+ "If specified, this template will create order with the same summary": [
+ ""
+ ],
+ "Fixed price": [
+ ""
+ ],
+ "If specified, this template will create order with the same price": [
+ ""
+ ],
+ "Minimum age": [
+ ""
+ ],
+ "Is this contract restricted to some age?": [
+ ""
+ ],
+ "Payment timeout": [
+ ""
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ ""
+ ],
+ "Verification algorithm": [
+ ""
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ ""
+ ],
+ "Point-of-sale key": [
+ ""
+ ],
+ "Useful to validate the purchase": [
+ ""
+ ],
+ "generate random secret key": [
+ ""
+ ],
+ "random": [
+ ""
+ ],
+ "show secret key": [
+ ""
+ ],
+ "hide secret key": [
+ ""
+ ],
+ "hide": [
+ ""
+ ],
+ "show": [
+ ""
+ ],
+ "could not inform template": [
+ ""
+ ],
+ "Amount is required": [
+ ""
+ ],
+ "Order summary is required": [
+ ""
+ ],
+ "New order for template": [
+ ""
+ ],
+ "Amount of the order": [
+ ""
+ ],
+ "Order summary": [
+ ""
+ ],
+ "could not create order from template": [
+ ""
+ ],
+ "Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment.": [
+ ""
+ ],
+ "Fixed amount": [
+ ""
+ ],
+ "Default amount": [
+ ""
+ ],
+ "Default summary": [
+ ""
+ ],
+ "Print": [
+ ""
+ ],
+ "Setup TOTP": [
+ ""
+ ],
+ "Templates": [
+ ""
+ ],
+ "add new templates": [
+ ""
+ ],
+ "load more templates before the first one": [
+ ""
+ ],
+ "load newer templates": [
+ ""
+ ],
+ "delete selected templates from the database": [
+ ""
+ ],
+ "use template to create new order": [
+ ""
+ ],
+ "create qr code for the template": [
+ ""
+ ],
+ "load more templates after the last one": [
+ ""
+ ],
+ "load older templates": [
+ ""
+ ],
+ "There is no templates yet, add more pressing the + sign": [
+ ""
+ ],
+ "template delete successfully": [
+ ""
+ ],
+ "could not delete the template": [
+ ""
+ ],
+ "could not update template": [
+ ""
+ ],
+ "should be one of '%1$s'": [
+ ""
+ ],
+ "Webhook ID to use": [
+ ""
+ ],
+ "Event": [
+ ""
+ ],
+ "The event of the webhook: why the webhook is used": [
+ ""
+ ],
+ "Method": [
+ ""
+ ],
+ "Method used by the webhook": [
+ ""
+ ],
+ "URL": [
+ ""
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ ""
+ ],
+ "Header": [
+ ""
+ ],
+ "Header template of the webhook": [
+ ""
+ ],
+ "Body": [
+ ""
+ ],
+ "Body template by the webhook": [
+ ""
+ ],
+ "Webhooks": [
+ ""
+ ],
+ "add new webhooks": [
+ ""
+ ],
+ "load more webhooks before the first one": [
+ ""
+ ],
+ "load newer webhooks": [
+ ""
+ ],
+ "Event type": [
+ ""
+ ],
+ "delete selected webhook from the database": [
+ ""
+ ],
+ "load more webhooks after the last one": [
+ ""
+ ],
+ "load older webhooks": [
+ ""
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ ""
+ ],
+ "webhook delete successfully": [
+ ""
+ ],
+ "could not delete the webhook": [
+ ""
+ ],
+ "check the id, does not look valid": [
+ ""
+ ],
+ "should have 52 characters, current %1$s": [
+ ""
+ ],
+ "URL doesn't have the right format": [
+ ""
+ ],
+ "Credited bank account": [
+ ""
+ ],
+ "Select one account": [
+ ""
+ ],
+ "Bank account of the merchant where the payment was received": [
+ ""
+ ],
+ "Wire transfer ID": [
+ ""
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ ""
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ ""
+ ],
+ "Amount credited": [
+ ""
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ ""
+ ],
+ "could not inform transfer": [
+ ""
+ ],
+ "Transfers": [
+ ""
+ ],
+ "add new transfer": [
+ ""
+ ],
+ "load more transfers before the first one": [
+ ""
+ ],
+ "load newer transfers": [
+ ""
+ ],
+ "Credit": [
+ ""
+ ],
+ "Confirmed": [
+ ""
+ ],
+ "Verified": [
+ ""
+ ],
+ "Executed at": [
+ ""
+ ],
+ "yes": [
+ ""
+ ],
+ "no": [
+ ""
+ ],
+ "unknown": [
+ ""
+ ],
+ "delete selected transfer from the database": [
+ ""
+ ],
+ "load more transfer after the last one": [
+ ""
+ ],
+ "load older transfers": [
+ ""
+ ],
+ "There is no transfer yet, add more pressing the + sign": [
+ ""
+ ],
+ "filter by account address": [
+ ""
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ ""
+ ],
+ "only show wire transfers claimed by the exchange": [
+ ""
+ ],
+ "Unverified": [
+ ""
+ ],
+ "is not valid": [
+ ""
+ ],
+ "is not a number": [
+ ""
+ ],
+ "must be 1 or greater": [
+ ""
+ ],
+ "max 7 lines": [
+ ""
+ ],
+ "change authorization configuration": [
+ ""
+ ],
+ "Need to complete marked fields and choose authorization method": [
+ ""
+ ],
+ "This is not a valid bitcoin address.": [
+ ""
+ ],
+ "This is not a valid Ethereum address.": [
+ ""
+ ],
+ "IBAN numbers usually have more that 4 digits": [
+ ""
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ ""
+ ],
+ "IBAN country code not found": [
+ ""
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ ""
+ ],
+ "Target type": [
+ ""
+ ],
+ "Method to use for wire transfer": [
+ ""
+ ],
+ "Routing": [
+ ""
+ ],
+ "Routing number.": [
+ ""
+ ],
+ "Account": [
+ ""
+ ],
+ "Account number.": [
+ ""
+ ],
+ "Business Identifier Code.": [
+ ""
+ ],
+ "Bank Account Number.": [
+ ""
+ ],
+ "Unified Payment Interface.": [
+ ""
+ ],
+ "Bitcoin protocol.": [
+ ""
+ ],
+ "Ethereum protocol.": [
+ ""
+ ],
+ "Interledger protocol.": [
+ ""
+ ],
+ "Host": [
+ ""
+ ],
+ "Bank host.": [
+ ""
+ ],
+ "Bank account.": [
+ ""
+ ],
+ "Bank account owner's name.": [
+ ""
+ ],
+ "No accounts yet.": [
+ ""
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ ""
+ ],
+ "Business name": [
+ ""
+ ],
+ "Legal name of the business represented by this instance.": [
+ ""
+ ],
+ "Email": [
+ ""
+ ],
+ "Contact email": [
+ ""
+ ],
+ "Website URL": [
+ ""
+ ],
+ "URL.": [
+ ""
+ ],
+ "Logo": [
+ ""
+ ],
+ "Logo image.": [
+ ""
+ ],
+ "Bank account": [
+ ""
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ ""
+ ],
+ "Default max deposit fee": [
+ ""
+ ],
+ "Maximum deposit fees this merchant is willing to pay per order by default.": [
+ ""
+ ],
+ "Default max wire fee": [
+ ""
+ ],
+ "Maximum wire fees this merchant is willing to pay per wire transfer by default.": [
+ ""
+ ],
+ "Default wire fee amortization": [
+ ""
+ ],
+ "Number of orders excess wire transfer fees will be divided by to compute per order surcharge.": [
+ ""
+ ],
+ "Physical location of the merchant.": [
+ ""
+ ],
+ "Jurisdiction": [
+ ""
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ ""
+ ],
+ "Default payment delay": [
+ ""
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ ""
+ ],
+ "Default wire transfer delay": [
+ ""
+ ],
+ "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.": [
+ ""
+ ],
+ "Instance id": [
+ ""
+ ],
+ "Change the authorization method use for this instance.": [
+ ""
+ ],
+ "Manage access token": [
+ ""
+ ],
+ "Failed to create instance": [
+ ""
+ ],
+ "Login required": [
+ ""
+ ],
+ "Please enter your access token.": [
+ ""
+ ],
+ "Access Token": [
+ ""
+ ],
+ "The request to the backend take too long and was cancelled": [
+ ""
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ ""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ ""
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ ""
+ ],
+ "Access denied": [
+ ""
+ ],
+ "The access token provided is invalid.": [
+ ""
+ ],
+ "No 'default' instance configured yet.": [
+ ""
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ ""
+ ],
+ "The access token provided is invalid": [
+ ""
+ ],
+ "Hide for today": [
+ ""
+ ],
+ "Instance": [
+ ""
+ ],
+ "Settings": [
+ ""
+ ],
+ "Connection": [
+ ""
+ ],
+ "New": [
+ ""
+ ],
+ "List": [
+ ""
+ ],
+ "Log out": [
+ ""
+ ],
+ "Check your token is valid": [
+ ""
+ ],
+ "Couldn't access the server.": [
+ ""
+ ],
+ "Could not infer instance id from url %1$s": [
+ ""
+ ],
+ "Server not found": [
+ ""
+ ],
+ "Server response with an error code": [
+ ""
+ ],
+ "Got message %1$s from %2$s": [
+ ""
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ ""
+ ],
+ "Unexpected Error": [
+ ""
+ ],
+ "The value %1$s is invalid for a payment url": [
+ ""
+ ],
+ "add element to the list": [
+ ""
+ ],
+ "add": [
+ ""
+ ],
+ "Deleting": [
+ ""
+ ],
+ "Changing": [
+ ""
+ ],
+ "Order ID": [
+ ""
+ ],
+ "Payment URL": [
+ ""
+ ]
+ }
+ }
+};
+
diff --git a/packages/auditor-backoffice-ui/src/i18n/sv.po b/packages/auditor-backoffice-ui/src/i18n/sv.po
new file mode 100644
index 000000000..d8d0bae29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/sv.po
@@ -0,0 +1,2741 @@
+# This file is part of TALER
+# (C) 2016 GNUnet e.V.
+#
+# 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.
+#
+# 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
+# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with "
+"enough entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this "
+"contract. If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize "
+"wire fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid ""
+"after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid ""
+"how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid ""
+"sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid ""
+"product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid ""
+"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid ""
+"Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid ""
+"Maximum wire fees this merchant is willing to pay per wire transfer by "
+"default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
diff --git a/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
new file mode 100644
index 000000000..5ef56ca05
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
@@ -0,0 +1,2726 @@
+# 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/>
+
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:84
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/components/modal/index.tsx:124
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/components/modal/index.tsx:178
+#, c-format
+msgid "Clear"
+msgstr ""
+
+#: src/components/modal/index.tsx:190
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/components/modal/index.tsx:296
+#, c-format
+msgid "is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:299
+#, c-format
+msgid "cannot be empty"
+msgstr ""
+
+#: src/components/modal/index.tsx:301
+#, c-format
+msgid "cannot be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:305
+#, c-format
+msgid "is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:315
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr ""
+
+#: src/components/modal/index.tsx:331
+#, c-format
+msgid "Old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:332
+#, c-format
+msgid "access token currently in use"
+msgstr ""
+
+#: src/components/modal/index.tsx:338
+#, c-format
+msgid "New access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:339
+#, c-format
+msgid "next access token to be used"
+msgstr ""
+
+#: src/components/modal/index.tsx:344
+#, c-format
+msgid "Repeat access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:345
+#, c-format
+msgid "confirm the same access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:350
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:377
+#, c-format
+msgid "cannot be the same as the old access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:394
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:420
+#, c-format
+msgid "With external authorization method no check will be done by the merchant backend"
+msgstr ""
+
+#: src/components/modal/index.tsx:436
+#, c-format
+msgid "Set external authorization"
+msgstr ""
+
+#: src/components/modal/index.tsx:448
+#, c-format
+msgid "Set access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:470
+#, c-format
+msgid "Operation in progress..."
+msgstr ""
+
+#: src/components/modal/index.tsx:479
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:80
+#, c-format
+msgid "Instances"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:93
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:99
+#, c-format
+msgid "add new instance"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:178
+#, c-format
+msgid "ID"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:181
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:220
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:237
+#, c-format
+msgid "Purge"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:261
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:68
+#, c-format
+msgid "Only show active instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:71
+#, c-format
+msgid "Active"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:78
+#, c-format
+msgid "Only show deleted instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:81
+#, c-format
+msgid "Deleted"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:88
+#, c-format
+msgid "Show all instances"
+msgstr ""
+
+#: src/paths/admin/list/View.tsx:91
+#, c-format
+msgid "All"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:101
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:106
+#, c-format
+msgid "Failed to delete instance"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:124
+#, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgstr ""
+
+#: src/paths/admin/list/index.tsx:129
+#, c-format
+msgid "Failed to purge instance"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:66
+#, c-format
+msgid "Timed out"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:103
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:106
+#, c-format
+msgid "Target account"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:109
+#, c-format
+msgid "KYC URL"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:144
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:147
+#, c-format
+msgid "Http Status"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:177
+#, c-format
+msgid "No pending kyc verification!"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:123
+#, c-format
+msgid "change value to unknown date"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:124
+#, c-format
+msgid "change value to empty"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:131
+#, c-format
+msgid "clear"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:136
+#, c-format
+msgid "change value to never"
+msgstr ""
+
+#: src/components/form/InputDate.tsx:141
+#, c-format
+msgid "never"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:49
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:66
+#, c-format
+msgid "Product id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:69
+#, c-format
+msgid "Description"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:94
+#, c-format
+msgid "Product"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:95
+#, c-format
+msgid "search products by it's description or id"
+msgstr ""
+
+#: src/components/form/InputSearchProduct.tsx:151
+#, c-format
+msgid "no products found with that description"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:56
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:64
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:76
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:109
+#, c-format
+msgid "Quantity"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:110
+#, c-format
+msgid "how many products will be added"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:117
+#, c-format
+msgid "Add from inventory"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:105
+#, c-format
+msgid "Image should be smaller than 1 MB"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:110
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/components/form/InputImage.tsx:115
+#, c-format
+msgid "Remove"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:113
+#, c-format
+msgid "No taxes configured for this product."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:119
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:120
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:122
+#, c-format
+msgid "Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:131
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:137
+#, c-format
+msgid "add tax to the tax list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:72
+#, c-format
+msgid "describe and add a product that is not in the inventory list"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:75
+#, c-format
+msgid "Add custom product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:86
+#, c-format
+msgid "Complete information of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, c-format
+msgid "photo of the product"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, c-format
+msgid "full product description"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, c-format
+msgid "name of the product unit"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:201
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:202
+#, c-format
+msgid "amount in the current currency"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:38
+#, c-format
+msgid "image"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:41
+#, c-format
+msgid "description"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:44
+#, c-format
+msgid "quantity"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:47
+#, c-format
+msgid "unit price"
+msgstr ""
+
+#: src/components/product/ProductList.tsx:50
+#, c-format
+msgid "total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:153
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:157
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:159
+#, c-format
+msgid "must be greater than 0"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:164
+#, c-format
+msgid "not a valid json"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:170
+#, c-format
+msgid "should be in the future"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, c-format
+msgid "refund deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, c-format
+msgid "wire transfer deadline cannot be before refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:190
+#, c-format
+msgid "wire transfer deadline cannot be before pay deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:197
+#, c-format
+msgid "should have a refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:202
+#, c-format
+msgid "auto refund cannot be after refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:360
+#, c-format
+msgid "Manage products in order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:369
+#, c-format
+msgid "Manage list of products in the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:391
+#, c-format
+msgid "Remove this product from the order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:415
+#, c-format
+msgid "Total price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:417
+#, c-format
+msgid "total product price added up"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:430
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:436
+#, c-format
+msgid "Order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:437
+#, c-format
+msgid "final order price"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:444
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:445
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:450
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:455
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:456
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, c-format
+msgid "Location"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:462
+#, c-format
+msgid "address where the products will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:469
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:470
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:476
+#, c-format
+msgid "Taler payment options"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:477
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, c-format
+msgid "Payment deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:482
+#, c-format
+msgid ""
+"Deadline for the customer to pay for the offer before it expires. Inventory "
+"products will be reserved until this deadline."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:486
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:487
+#, c-format
+msgid "Time until which the order can be refunded by the merchant."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:491
+#, c-format
+msgid "Wire transfer deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:492
+#, c-format
+msgid "Deadline for the exchange to make the wire transfer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:496
+#, c-format
+msgid "Auto-refund deadline"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:497
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without user "
+"interaction."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:502
+#, c-format
+msgid "Maximum deposit fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:503
+#, c-format
+msgid ""
+"Maximum deposit fees the merchant is willing to cover for this order. Higher "
+"deposit fees must be covered in full by the consumer."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, c-format
+msgid "Maximum wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:508
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:512
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:513
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:517
+#, c-format
+msgid "Create token"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:518
+#, c-format
+msgid ""
+"Uncheck this option if the merchant backend generated an order ID with enough "
+"entropy to prevent adversarial claims."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:522
+#, c-format
+msgid "Minimum age required"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid ""
+"Any value greater than 0 will limit the coins able be used to pay this contract. "
+"If empty the age restriction will be defined by the products"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:526
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:534
+#, c-format
+msgid "Additional information"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:541
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:55
+#, c-format
+msgid "days"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:65
+#, c-format
+msgid "hours"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:76
+#, c-format
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:53
+#, c-format
+msgid "forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:64
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:66
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:68
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:70
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:72
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:75
+#, c-format
+msgid "Orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:81
+#, c-format
+msgid "create order"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "load newer orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:154
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:200
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:209
+#, c-format
+msgid "copy url"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:225
+#, c-format
+msgid "load older orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:242
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:288
+#, c-format
+msgid "duplicated"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:299
+#, c-format
+msgid "invalid format"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:301
+#, c-format
+msgid "this value exceed the refundable amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:346
+#, c-format
+msgid "date"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:349
+#, c-format
+msgid "amount"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:352
+#, c-format
+msgid "reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:389
+#, c-format
+msgid "amount to be refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, c-format
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:396
+#, c-format
+msgid "Reason"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:397
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:399
+#, c-format
+msgid "requested by the customer"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:400
+#, c-format
+msgid "other"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:403
+#, c-format
+msgid "why this order is being refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:409
+#, c-format
+msgid "more information to give context"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:62
+#, c-format
+msgid "Contract Terms"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:68
+#, c-format
+msgid "human-readable description of the whole purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:74
+#, c-format
+msgid "total price for the transaction"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:81
+#, c-format
+msgid "URL for this purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:87
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:88
+#, c-format
+msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:93
+#, c-format
+msgid "Max wire fee"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:94
+#, c-format
+msgid "maximum wire fee accepted by the merchant"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:100
+#, c-format
+msgid ""
+"over how many customer transactions does the merchant expect to amortize wire "
+"fees on average"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:105
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:106
+#, c-format
+msgid "time when this contract was generated"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:112
+#, c-format
+msgid "after this deadline has passed no refunds will be accepted"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:118
+#, c-format
+msgid "after this deadline, the merchant won't accept payments for the contract"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:124
+#, c-format
+msgid "transfer deadline for the exchange"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:130
+#, c-format
+msgid "time indicating when the order should be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:136
+#, c-format
+msgid "where the order will be delivered"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:144
+#, c-format
+msgid "Auto-refund delay"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:145
+#, c-format
+msgid "how long the wallet should try to get an automatic refund for the purchase"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:150
+#, c-format
+msgid "Extra info"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:151
+#, c-format
+msgid "extra data that is only interpreted by the merchant frontend"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:219
+#, c-format
+msgid "Order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:221
+#, c-format
+msgid "claimed"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:247
+#, c-format
+msgid "claimed at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:265
+#, c-format
+msgid "Timeline"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:271
+#, c-format
+msgid "Payment details"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:291
+#, c-format
+msgid "Order status"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:301
+#, c-format
+msgid "Product list"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:451
+#, c-format
+msgid "paid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:455
+#, c-format
+msgid "wired"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:460
+#, c-format
+msgid "refunded"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:480
+#, c-format
+msgid "refund order"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:481
+#, c-format
+msgid "not refundable"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:489
+#, c-format
+msgid "refund"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:553
+#, c-format
+msgid "Refunded amount"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:560
+#, c-format
+msgid "Refund taken"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:570
+#, c-format
+msgid "Status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:583
+#, c-format
+msgid "Refund URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:636
+#, c-format
+msgid "unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:654
+#, c-format
+msgid "pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:666
+#, c-format
+msgid "created at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:707
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:711
+#, c-format
+msgid "Payment URI"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:740
+#, c-format
+msgid "Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:767
+#, c-format
+msgid "Back"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:79
+#, c-format
+msgid "refund created successfully"
+msgstr ""
+
+#: src/paths/instance/orders/details/index.tsx:85
+#, c-format
+msgid "could not create the refund"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:78
+#, c-format
+msgid "select date to show nearby orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:94
+#, c-format
+msgid "order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:100
+#, c-format
+msgid "jump to order with the given order ID"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:122
+#, c-format
+msgid "remove all filters"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:132
+#, c-format
+msgid "only show paid orders"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:135
+#, c-format
+msgid "Paid"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:142
+#, c-format
+msgid "only show orders with refunds"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:145
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:152
+#, c-format
+msgid ""
+"only show orders where customers paid, but wire payments from payment provider "
+"are still pending"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:155
+#, c-format
+msgid "Not wired"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:170
+#, c-format
+msgid "clear date filter"
+msgstr ""
+
+#: src/paths/instance/orders/list/ListPage.tsx:184
+#, c-format
+msgid "date (YYYY/MM/DD)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:103
+#, c-format
+msgid "Enter an order id"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:111
+#, c-format
+msgid "order not found"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:178
+#, c-format
+msgid "could not get the order to refund"
+msgstr ""
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, c-format
+msgid ""
+"click here to configure the stock of the product, leave it as is and the backend "
+"will not control stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:109
+#, c-format
+msgid "Manage stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:115
+#, c-format
+msgid "this product has been configured without stock control"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:119
+#, c-format
+msgid "Infinite"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:136
+#, c-format
+msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:176
+#, c-format
+msgid "Incoming"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:177
+#, c-format
+msgid "Lost"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:192
+#, c-format
+msgid "Current"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:196
+#, c-format
+msgid "remove stock control for this product"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:202
+#, c-format
+msgid "without stock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:211
+#, c-format
+msgid "Next restock"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:217
+#, c-format
+msgid "Delivery address"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:133
+#, c-format
+msgid "product identification to use in URLs (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:139
+#, c-format
+msgid "illustration of the product for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:145
+#, c-format
+msgid "product description for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:149
+#, c-format
+msgid "Age restricted"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:150
+#, c-format
+msgid "is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:155
+#, c-format
+msgid ""
+"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 "
+"meters) for customers"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:160
+#, c-format
+msgid "sale price for customers, including taxes, for above units of the product"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:164
+#, c-format
+msgid "Stock"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:166
+#, c-format
+msgid "product inventory for products with finite supply (for internal use only)"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:171
+#, c-format
+msgid "taxes included in the product price, exposed to customers"
+msgstr ""
+
+#: src/paths/instance/products/create/CreatePage.tsx:66
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/products/create/index.tsx:51
+#, c-format
+msgid "could not create product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:68
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "add product to inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:137
+#, c-format
+msgid "Sell"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:143
+#, c-format
+msgid "Profit"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:149
+#, c-format
+msgid "Sold"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:210
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:248
+#, c-format
+msgid "go to product update page"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:255
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:260
+#, c-format
+msgid "remove this product from the database"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:331
+#, c-format
+msgid "update the product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:341
+#, c-format
+msgid "update product with new price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:399
+#, c-format
+msgid "add more elements to the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:404
+#, c-format
+msgid "report elements lost in the inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:409
+#, c-format
+msgid "new price for the product"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:421
+#, c-format
+msgid "the are value with errors"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:422
+#, c-format
+msgid "update product with new stock and price"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:463
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:86
+#, c-format
+msgid "product updated successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:92
+#, c-format
+msgid "could not update the product"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:103
+#, c-format
+msgid "product delete successfully"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:109
+#, c-format
+msgid "could not delete the product"
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#, c-format
+msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#, c-format
+msgid "it should be greater than 0"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#, c-format
+msgid "must be a valid URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#, c-format
+msgid "Initial balance"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#, c-format
+msgid "balance prior to deposit"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#, c-format
+msgid "URL of exchange"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#, c-format
+msgid "Wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#, c-format
+msgid "method to use for wire transfer"
+msgstr ""
+
+#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#, c-format
+msgid "Select one wire method"
+msgstr ""
+
+#: src/paths/instance/reserves/create/index.tsx:62
+#, c-format
+msgid "could not create reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#, c-format
+msgid "Created balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#, c-format
+msgid "Exchange balance"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#, c-format
+msgid "Picked up"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#, c-format
+msgid "Committed"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#, c-format
+msgid "Account address"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#, c-format
+msgid "Tips"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#, c-format
+msgid "No tips has been authorized from this reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#, c-format
+msgid "Authorized"
+msgstr ""
+
+#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#, c-format
+msgid "Expiration"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#, c-format
+msgid "amount of tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#, c-format
+msgid "Justification"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#, c-format
+msgid "reason for the tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#, c-format
+msgid "URL after tip"
+msgstr ""
+
+#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#, c-format
+msgid "URL to visit after tip payment"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:65
+#, c-format
+msgid "Reserves not yet funded"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:89
+#, c-format
+msgid "Reserves ready"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:95
+#, c-format
+msgid "add new reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:143
+#, c-format
+msgid "Expires at"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:146
+#, c-format
+msgid "Initial"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:202
+#, c-format
+msgid "delete selected reserve from the database"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:210
+#, c-format
+msgid "authorize new tip from selected reserve"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:237
+#, c-format
+msgid "There is no ready reserves yet, add more pressing the + sign or fund them"
+msgstr ""
+
+#: src/paths/instance/reserves/list/Table.tsx:264
+#, c-format
+msgid "Expected Balance"
+msgstr ""
+
+#: src/paths/instance/reserves/list/index.tsx:110
+#, c-format
+msgid "could not create the tip"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:77
+#, c-format
+msgid "should not be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:93
+#, c-format
+msgid "should be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "can't be empty"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:100
+#, c-format
+msgid "to short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:108
+#, c-format
+msgid "just letters and numbers from 2 to 7"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:110
+#, c-format
+msgid "size of the key should be 32"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:137
+#, c-format
+msgid "Identifier"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:138
+#, c-format
+msgid "Name of the template in URLs."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:144
+#, c-format
+msgid "Describe what this template stands for"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:149
+#, c-format
+msgid "Fixed summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:150
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:154
+#, c-format
+msgid "Fixed price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:155
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:159
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:161
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:165
+#, c-format
+msgid "Payment timeout"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:167
+#, c-format
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:171
+#, c-format
+msgid "Verification algorithm"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:172
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:180
+#, c-format
+msgid "Point-of-sale key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:182
+#, c-format
+msgid "Useful to validate the purchase"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:196
+#, c-format
+msgid "generate random secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:203
+#, c-format
+msgid "random"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:208
+#, c-format
+msgid "show secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:209
+#, c-format
+msgid "hide secret key"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:216
+#, c-format
+msgid "hide"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:218
+#, c-format
+msgid "show"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "could not inform template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:54
+#, c-format
+msgid "Amount is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Order summary is required"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:86
+#, c-format
+msgid "New order for template"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:108
+#, c-format
+msgid "Amount of the order"
+msgstr ""
+
+#: src/paths/instance/templates/use/UsePage.tsx:113
+#, c-format
+msgid "Order summary"
+msgstr ""
+
+#: src/paths/instance/templates/use/index.tsx:92
+#, c-format
+msgid "could not create order from template"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:131
+#, c-format
+msgid ""
+"Here you can specify a default value for fields that are not fixed. Default "
+"values can be edited by the customer before the payment."
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:148
+#, c-format
+msgid "Fixed amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:149
+#, c-format
+msgid "Default amount"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:161
+#, c-format
+msgid "Default summary"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:177
+#, c-format
+msgid "Print"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:184
+#, c-format
+msgid "Setup TOTP"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:65
+#, c-format
+msgid "Templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:70
+#, c-format
+msgid "add new templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:142
+#, c-format
+msgid "load more templates before the first one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:146
+#, c-format
+msgid "load newer templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:181
+#, c-format
+msgid "delete selected templates from the database"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:188
+#, c-format
+msgid "use template to create new order"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:195
+#, c-format
+msgid "create qr code for the template"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:210
+#, c-format
+msgid "load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "load older templates"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:231
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:104
+#, c-format
+msgid "template delete successfully"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:110
+#, c-format
+msgid "could not delete the template"
+msgstr ""
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, c-format
+msgid "could not update template"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#, c-format
+msgid "should be one of '%1$s'"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#, c-format
+msgid "Method"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "Method used by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "Header"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#, c-format
+msgid "Header template of the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#, c-format
+msgid "Body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#, c-format
+msgid "Body template by the webhook"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:61
+#, c-format
+msgid "Webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:66
+#, c-format
+msgid "add new webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:137
+#, c-format
+msgid "load more webhooks before the first one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:141
+#, c-format
+msgid "load newer webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:151
+#, c-format
+msgid "Event type"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:176
+#, c-format
+msgid "delete selected webhook from the database"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:198
+#, c-format
+msgid "load more webhooks after the last one"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:202
+#, c-format
+msgid "load older webhooks"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/Table.tsx:219
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:94
+#, c-format
+msgid "webhook delete successfully"
+msgstr ""
+
+#: src/paths/instance/webhooks/list/index.tsx:100
+#, c-format
+msgid "could not delete the webhook"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#, c-format
+msgid "check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#, c-format
+msgid "should have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Credited bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#, c-format
+msgid "Select one account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#, c-format
+msgid "Wire transfer ID"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#, c-format
+msgid ""
+"unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the wire "
+"transfer subject"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#, c-format
+msgid "Amount credited"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/create/index.tsx:58
+#, c-format
+msgid "could not inform transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:61
+#, c-format
+msgid "Transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:66
+#, c-format
+msgid "add new transfer"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:129
+#, c-format
+msgid "load more transfers before the first one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "load newer transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:143
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:152
+#, c-format
+msgid "Confirmed"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "Verified"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:158
+#, c-format
+msgid "Executed at"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "yes"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:171
+#, c-format
+msgid "no"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:187
+#, c-format
+msgid "delete selected transfer from the database"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:202
+#, c-format
+msgid "load more transfer after the last one"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:206
+#, c-format
+msgid "load older transfers"
+msgstr ""
+
+#: src/paths/instance/transfers/list/Table.tsx:223
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:79
+#, c-format
+msgid "filter by account address"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:100
+#, c-format
+msgid "only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:110
+#, c-format
+msgid "only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:113
+#, c-format
+msgid "Unverified"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:69
+#, c-format
+msgid "is not valid"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:94
+#, c-format
+msgid "is not a number"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:96
+#, c-format
+msgid "must be 1 or greater"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:107
+#, c-format
+msgid "max 7 lines"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:178
+#, c-format
+msgid "change authorization configuration"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:217
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:82
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:95
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:118
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:120
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:128
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:153
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:248
+#, c-format
+msgid "Target type"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:249
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:258
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:259
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:263
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:264
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:273
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:282
+#, c-format
+msgid "Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:292
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:301
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:319
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:328
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:329
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:334
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:343
+#, c-format
+msgid "Bank account owner's name."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:370
+#, c-format
+msgid "No accounts yet."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it is "
+"used to administer other instances."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#, c-format
+msgid "Business name"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#, c-format
+msgid "Contact email"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#, c-format
+msgid "Website URL"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#, c-format
+msgid "URL."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#, c-format
+msgid "Logo"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#, c-format
+msgid "Logo image."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#, c-format
+msgid "URI specifying bank account for crediting revenue."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#, c-format
+msgid "Default max deposit fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#, c-format
+msgid "Maximum deposit fees this merchant is willing to pay per order by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Default max wire fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#, c-format
+msgid "Maximum wire fees this merchant is willing to pay per wire transfer by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#, c-format
+msgid "Default wire fee amortization"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid ""
+"Number of orders excess wire transfer fees will be divided by to compute per "
+"order surcharge."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Physical location of the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Jurisdiction"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#, c-format
+msgid "Default payment delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#, c-format
+msgid "Time customers have to pay an order before the offer expires by default."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#, c-format
+msgid "Default wire transfer delay"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#, c-format
+msgid ""
+"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."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:164
+#, c-format
+msgid "Instance id"
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:173
+#, c-format
+msgid "Change the authorization method use for this instance."
+msgstr ""
+
+#: src/paths/instance/update/UpdatePage.tsx:182
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/instance/update/index.tsx:112
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/components/exception/login.tsx:74
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/components/exception/login.tsx:80
+#, c-format
+msgid "Please enter your access token."
+msgstr ""
+
+#: src/components/exception/login.tsx:108
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:171
+#, c-format
+msgid "The request to the backend take too long and was cancelled"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:172
+#, c-format
+msgid "Diagnostic from %1$s is \"%2$s\""
+msgstr ""
+
+#: src/InstanceRoutes.tsx:178
+#, c-format
+msgid "The backend reported a problem: HTTP status #%1$s"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:179
+#, c-format
+msgid "Diagnostic from %1$s is '%2$s'"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:196
+#, c-format
+msgid "Access denied"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:197
+#, c-format
+msgid "The access token provided is invalid."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:212
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:213
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/InstanceRoutes.tsx:630
+#, c-format
+msgid "The access token provided is invalid"
+msgstr ""
+
+#: src/InstanceRoutes.tsx:664
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:82
+#, c-format
+msgid "Instance"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:91
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:167
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:209
+#, c-format
+msgid "New"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:219
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:234
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:71
+#, c-format
+msgid "Check your token is valid"
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:90
+#, c-format
+msgid "Couldn't access the server."
+msgstr ""
+
+#: src/ApplicationReadyRoutes.tsx:91
+#, c-format
+msgid "Could not infer instance id from url %1$s"
+msgstr ""
+
+#: src/Application.tsx:104
+#, c-format
+msgid "Server not found"
+msgstr ""
+
+#: src/Application.tsx:118
+#, c-format
+msgid "Server response with an error code"
+msgstr ""
+
+#: src/Application.tsx:120
+#, c-format
+msgid "Got message %1$s from %2$s"
+msgstr ""
+
+#: src/Application.tsx:131
+#, c-format
+msgid "Response from server is unreadable, http status: %1$s"
+msgstr ""
+
+#: src/Application.tsx:144
+#, c-format
+msgid "Unexpected Error"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:101
+#, c-format
+msgid "The value %1$s is invalid for a payment url"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:110
+#, c-format
+msgid "add element to the list"
+msgstr ""
+
+#: src/components/form/InputArray.tsx:112
+#, c-format
+msgid "add"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#, c-format
+msgid "Order ID"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#, c-format
+msgid "Payment URL"
+msgstr ""
+
diff --git a/packages/auditor-backoffice-ui/src/index.html b/packages/auditor-backoffice-ui/src/index.html
new file mode 100644
index 000000000..d79bdf130
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/index.html
@@ -0,0 +1,45 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!DOCTYPE html>
+<html
+ lang="en"
+ class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
+>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Auditor Backoffice</title>
+ <!-- Optional customization script. -->
+ <script src="auditor-backoffice-ui-settings.js"></script>
+ <!-- Entry point for the demobank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+ <body>
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/auditor-backoffice-ui/src/index.tsx b/packages/auditor-backoffice-ui/src/index.tsx
new file mode 100644
index 000000000..7fdf7c1c3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/index.tsx
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Application } from "./Application.js";
+
+import { h, render } from "preact";
+import "./scss/main.scss";
+
+const app = document.getElementById("app");
+
+render(<Application />, app as any);
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
new file mode 100644
index 000000000..91b6b4b56
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -0,0 +1,57 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "ARS",
+ version: "1",
+ }}
+ >
+ <Component {...args} />
+ </ConfigContextProvider>
+ );
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
+// export const Example = (a: any): VNode => <CreatePage {...a} />;
+// Example.args = {
+// isLoading: false
+// }
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
new file mode 100644
index 000000000..d13b7e929
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -0,0 +1,257 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ auth_token?: string;
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+interface Props {
+ onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>;
+ onBack?: () => void;
+ forceId?: string;
+}
+
+function with_defaults(id?: string): Partial<Entity> {
+ return {
+ id,
+ // accounts: [],
+ user_type: "business",
+ use_stefan: true,
+ default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours
+ default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days
+ };
+}
+
+export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
+ const [value, valueHandler] = useState(with_defaults(forceId));
+ const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
+ const [isTokenDialogActive, updateIsTokenDialogActive] =
+ useState<boolean>(false);
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ id: !value.id
+ ? i18n.str`required`
+ : !INSTANCE_ID_REGEX.test(value.id)
+ ? i18n.str`is not valid`
+ : undefined,
+ name: !value.name ? i18n.str`required` : undefined,
+
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ // accounts:
+ // !value.accounts || !value.accounts.length
+ // ? i18n.str`required`
+ // : undefinedIfEmpty(
+ // value.accounts.map((p) => {
+ // return !PAYTO_REGEX.test(p.payto_uri)
+ // ? i18n.str`is not valid`
+ // : undefined;
+ // }),
+ // ),
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): Promise<void> => {
+ // use conversion instead of this
+ const newToken = value.auth_token;
+ value.auth_token = undefined;
+ value.auth = newToken === null || newToken === undefined
+ ? { method: "external" }
+ : { method: "token", token: `secret-token:${newToken}` };
+ if (!value.address) value.address = {};
+ if (!value.jurisdiction) value.jurisdiction = {};
+ // remove above use conversion
+ // schema.validateSync(value, { abortEarly: false })
+ value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any
+ value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any
+ // delete value.default_pay_delay;
+ // delete value.default_wire_transfer_delay;
+
+ return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage);
+ };
+
+ function updateToken(token: string | null) {
+ valueHandler((old) => ({
+ ...old,
+ auth_token: token === null ? undefined : token,
+ }));
+ }
+
+ return (
+ <div>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {isTokenDialogActive && (
+ <SetTokenNewInstanceModal
+ onCancel={() => {
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(false);
+ }}
+ onClear={() => {
+ updateToken(null);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ onConfirm={(newToken) => {
+ updateToken(newToken);
+ updateIsTokenDialogActive(false);
+ updateIsTokenSet(true);
+ }}
+ />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} />
+ </FormProvider>
+
+ <div class="level">
+ <div class="level-item has-text-centered">
+ <h1 class="title">
+ <button
+ class={
+ !isTokenSet
+ ? "button is-danger has-tooltip-bottom"
+ : !value.auth_token
+ ? "button has-tooltip-bottom"
+ : "button is-info has-tooltip-bottom"
+ }
+ data-tooltip={i18n.str`change authorization configuration`}
+ onClick={() => updateIsTokenDialogActive(true)}
+ >
+ <div class="icon is-centered">
+ <i class="mdi mdi-lock-reset" />
+ </div>
+ <span>
+ <i18n.Translate>Set access token</i18n.Translate>
+ </span>
+ </button>
+ </h1>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-item has-text-centered">
+ {!isTokenSet ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is not yet configured. This instance can't be
+ created.
+ </i18n.Translate>
+ </p>
+ ) : value.auth_token === undefined ? (
+ <p class="is-size-6">
+ <i18n.Translate>
+ No access token. Authorization must be handled externally.
+ </i18n.Translate>
+ </p>
+ ) : (
+ <p class="is-size-6">
+ <i18n.Translate>
+ Access token is set. Authorization is handled by the
+ merchant backend.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+ </div>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submit}
+ disabled={hasErrors || !isTokenSet}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields and choose authorization method`
+ : "confirm operation"
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
new file mode 100644
index 000000000..c620c6482
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
@@ -0,0 +1,74 @@
+/*
+ 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)
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+
+export function InstanceCreatedSuccessfully({
+ entity,
+ onConfirm,
+}: {
+ entity: Entity;
+ onConfirm: () => void;
+}): VNode {
+ return (
+ <CreatedSuccessfully onConfirm={onConfirm}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Business Name</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.name} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Access token</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ {entity.auth.method === "external" && "external"}
+ {entity.auth.method === "token" && (
+ <input class="input" readonly value={entity.auth.token} />
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
new file mode 100644
index 000000000..23f41ecff
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
@@ -0,0 +1,82 @@
+/*
+ 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)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useCredentialsChecker } from "../../../hooks/backend.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ forceId?: string;
+}
+export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage;
+
+export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
+ const { createInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const { requestNewLoginToken } = useCredentialsChecker()
+ const { url: backendURL, updateToken } = useBackendContext()
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ forceId={forceId}
+ onCreate={async (
+ d: MerchantBackend.Instances.InstanceConfigurationMessage,
+ ) => {
+ try {
+ await createInstance(d)
+ if (d.auth.token) {
+ const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+ }
+ onConfirm();
+ } catch (ex) {
+ if (ex instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: ex.message,
+ });
+ } else {
+ console.error(ex)
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
new file mode 100644
index 000000000..0012f9b9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -0,0 +1,52 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Instance/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
new file mode 100644
index 000000000..fdae1a24d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
@@ -0,0 +1,18 @@
+/*
+ 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/>
+ */
+
+// export * as list from "./list/stories.js";
+export * as create from "./create/stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
new file mode 100644
index 000000000..885a351d2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -0,0 +1,287 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ onCreate: () => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onUpdate,
+ onPurge,
+ setInstanceName,
+ onDelete,
+ selected,
+}: Props): VNode {
+ const [actionQueue, actionQueueHandler] = useState<Actions[]>([]);
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "DELETE"
+ ) {
+ onDelete(actionQueue[0].element);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onDelete]);
+
+ useEffect(() => {
+ if (
+ actionQueue.length > 0 &&
+ !selected &&
+ actionQueue[0].type == "UPDATE"
+ ) {
+ onUpdate(actionQueue[0].element.id);
+ actionQueueHandler(actionQueue.slice(1));
+ }
+ }, [actionQueue, selected, onUpdate]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-desktop-mac" />
+ </span>
+ <i18n.Translate>Instances</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options">
+ <button
+ class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"}
+ type="button"
+ onClick={(): void =>
+ actionQueueHandler(
+ buildActions(instances, rowSelection, "DELETE"),
+ )
+ }
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new instance`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onPurge={onPurge}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: MerchantBackend.Instances.Instance[];
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ setInstanceName: (s: string) => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ setInstanceName,
+ instances,
+ onUpdate,
+ onDelete,
+ onPurge,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.length === instances.length}
+ onClick={(): void =>
+ rowSelectionHandler(
+ rowSelection.length === instances.length
+ ? []
+ : instances.map((i) => i.id),
+ )
+ }
+ />
+ <span class="check" />
+ </label>
+ </th>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td class="is-checkbox-cell">
+ <label class="b-checkbox checkbox">
+ <input
+ type="checkbox"
+ checked={rowSelection.indexOf(i.id) != -1}
+ onClick={(): void =>
+ rowSelectionHandler(toggleSelected(i.id))
+ }
+ />
+ <span class="check" />
+ </label>
+ </td>
+ <td>
+ <a
+ href={`#/orders?instance=${i.id}`}
+ onClick={(e) => {
+ setInstanceName(i.id);
+ }}
+ >
+ {i.id}
+ </a>
+ </td>
+ <td>{i.name}</td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-success jb-modal"
+ type="button"
+ onClick={(): void => onUpdate(i.id)}
+ >
+ <i18n.Translate>Edit</i18n.Translate>
+ </button>
+ {!i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal is-outlined"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ )}
+ {i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onPurge(i)}
+ >
+ <i18n.Translate>Purge</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no instances yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface Actions {
+ element: MerchantBackend.Instances.Instance;
+ type: "DELETE" | "UPDATE";
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+function buildActions(
+ instances: MerchantBackend.Instances.Instance[],
+ selected: string[],
+ action: "DELETE",
+): Actions[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
new file mode 100644
index 000000000..e0f5d5430
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ 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)
+ */
+
+import { h } from "preact";
+import { View } from "./View.js";
+
+export default {
+ title: "Pages/Instance/List",
+ component: View,
+ argTypes: {
+ onSelect: { action: "onSelect" },
+ },
+};
+
+export const Empty = (a: any) => <View {...a} />;
+Empty.args = {
+ instances: [],
+};
+
+export const WithDefaultInstance = (a: any) => <View {...a} />;
+WithDefaultInstance.args = {
+ instances: [
+ {
+ id: "default",
+ name: "the default instance",
+ merchant_pub: "abcdef",
+ payment_targets: [],
+ },
+ ],
+};
+
+export const WithFiveInstance = (a: any) => <View {...a} />;
+WithFiveInstance.args = {
+ instances: [
+ {
+ id: "first",
+ name: "the first instance",
+ merchant_pub: "abcdefgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "second",
+ name: "the second instance",
+ merchant_pub: "zxczxcz",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "third",
+ name: "the third instance",
+ merchant_pub: "QWEQWEWQE",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "other",
+ name: "the other instance",
+ merchant_pub: "FHJHGJGHJ",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "another",
+ name: "the another instance",
+ merchant_pub: "abcd3423423efgh",
+ payment_targets: ["asd"],
+ },
+ {
+ id: "last",
+ name: "last instance",
+ merchant_pub: "zxcvvbnm",
+ payment_targets: ["pay-to", "asd"],
+ },
+ ],
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
new file mode 100644
index 000000000..b59112338
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
@@ -0,0 +1,110 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+import { CardTable as CardTableActive } from "./TableActive.js";
+
+interface Props {
+ instances: MerchantBackend.Instances.Instance[];
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ onDelete: (id: MerchantBackend.Instances.Instance) => void;
+ onPurge: (id: MerchantBackend.Instances.Instance) => void;
+ selected?: boolean;
+ setInstanceName: (s: string) => void;
+}
+
+export function View({
+ instances,
+ onCreate,
+ onDelete,
+ onPurge,
+ onUpdate,
+ setInstanceName,
+ selected,
+}: Props): VNode {
+ const [show, setShow] = useState<"active" | "deleted" | null>("active");
+ const showIsActive = show === "active" ? "is-active" : "";
+ const showIsDeleted = show === "deleted" ? "is-active" : "";
+ const showAll = show === null ? "is-active" : "";
+ const { i18n } = useTranslationContext();
+
+ const showingInstances = showIsDeleted
+ ? instances.filter((i) => i.deleted)
+ : showIsActive
+ ? instances.filter((i) => !i.deleted)
+ : instances;
+
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={showIsActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show active instances`}
+ >
+ <a onClick={() => setShow("active")}>
+ <i18n.Translate>Active</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showIsDeleted}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show deleted instances`}
+ >
+ <a onClick={() => setShow("deleted")}>
+ <i18n.Translate>Deleted</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showAll}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Show all instances`}
+ >
+ <a onClick={() => setShow(null)}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <CardTableActive
+ instances={showingInstances}
+ onDelete={onDelete}
+ onPurge={onPurge}
+ setInstanceName={setInstanceName}
+ onUpdate={onUpdate}
+ selected={selected}
+ onCreate={onCreate}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
new file mode 100644
index 000000000..2f839291b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
@@ -0,0 +1,140 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { View } from "./View.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ instances: MerchantBackend.Instances.Instance[];
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ setInstanceName: (s: string) => void;
+}
+
+export default function Instances({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onCreate,
+ onUpdate,
+ setInstanceName,
+}: Props): VNode {
+ const result = useBackendInstances();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const [purging, setPurging] =
+ useState<MerchantBackend.Instances.Instance | null>(null);
+ const { deleteInstance, purgeInstance } = useAdminAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <View
+ instances={result.data.instances}
+ onDelete={setDeleting}
+ onCreate={onCreate}
+ onPurge={setPurging}
+ onUpdate={onUpdate}
+ setInstanceName={setInstanceName}
+ selected={!!deleting}
+ />
+ {deleting && (
+ <DeleteModal
+ element={deleting}
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance(deleting.id);
+ // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
+ setNotif({
+ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ // pushNotification({ message: 'delete_error', type: 'ERROR' })
+ }
+ setDeleting(null);
+ }}
+ />
+ )}
+ {purging && (
+ <PurgeModal
+ element={purging}
+ onCancel={() => setPurging(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await purgeInstance(purging.id);
+ setNotif({
+ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to purge instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setPurging(null);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
new file mode 100644
index 000000000..3336c53a4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Accounts/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
new file mode 100644
index 000000000..6e4786a47
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,173 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const accountAuthType = ["none", "basic"];
+
+function isValidURL(s: string): boolean {
+ try {
+ const u = new URL(s)
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+ const errors: FormErrors<Entity> = {
+ payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ username:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password
+ ? i18n.str`required`
+ : undefined,
+ }),
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
+ : undefined,
+ repeatPassword:
+ !state.credit_facade_credentials
+ ? undefined
+ : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword)
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ delete state.repeatPassword
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
new file mode 100644
index 000000000..7d33d25ce
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,65 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { useBankAccountAPI } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createBankAccount } = useBankAccountAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createBankAccount(request)
+ .then((d) => {
+ onConfirm()
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
new file mode 100644
index 000000000..6b4b63735
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Accounts/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
new file mode 100644
index 000000000..24da755b9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.BankAccounts.BankAccountEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+ onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ accounts={devices.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
new file mode 100644
index 000000000..7d6db0782
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -0,0 +1,385 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry;
+
+interface Props {
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ accounts,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Bank accounts</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new accounts`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {accounts.length > 0 ? (
+ <Table
+ accounts={accounts}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ accounts: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ accounts,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
+ const accountsByType = accounts.reduce((prev, acc) => {
+ const parsed = parsePaytoUri(acc.payto_uri)
+ if (!parsed) return prev //skip
+ if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") {
+ prev["unknown"].push({ parsed, acc })
+ } else {
+ prev[parsed.targetType].push({ parsed, acc })
+ }
+ return prev
+ }, emptyList)
+
+ const bitcoinAccounts = accountsByType["bitcoin"]
+ const talerbankAccounts = accountsByType["x-taler-bank"]
+ const ibanAccounts = accountsByType["iban"]
+ const unkownAccounts = accountsByType["unknown"]
+
+
+ return (
+ <Fragment>
+
+ {bitcoinAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 1</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 2</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {bitcoinAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriBitcoin
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[0]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[1]}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+
+
+ {talerbankAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Host</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {talerbankAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriTalerBank
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.host}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.account}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {ibanAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>IBAN</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>BIC</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {ibanAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriIBAN
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.params["receiver-name"]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.iban}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.bic ?? ""}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {unkownAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Path</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {unkownAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriUnknown
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetType}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+ </Fragment>
+
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no accounts yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
new file mode 100644
index 000000000..100241e22
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -0,0 +1,107 @@
+/*
+ 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)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteBankAccount } = useBankAccountAPI();
+ const result = useInstanceBankAccounts({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.accounts}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) =>
+ deleteBankAccount(e.h_wire)
+ .then(() =>
+ setNotif({
+ message: i18n.str`bank account delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the bank account`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
new file mode 100644
index 000000000..e0e0ba7ed
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -0,0 +1,195 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = MerchantBackend.BankAccounts.BankAccountEntry
+ & WithId;
+
+const accountAuthType = ["unedit", "none", "basic"];
+interface Props {
+ onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>;
+ onBack?: () => void;
+ account: Entity;
+}
+
+
+export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account);
+
+ const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = {
+ credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined,
+ credit_facade_credentials: undefinedIfEmpty({
+
+ username: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.username ? i18n.str`required` : undefined,
+
+ password: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !state.credit_facade_credentials.password ? i18n.str`required` : undefined,
+
+ repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined
+ : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` :
+ (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesnt match`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+
+ const creds: typeof state.credit_facade_credentials =
+ state.credit_facade_credentials?.type === "basic" ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ } : state.credit_facade_credentials?.type === "none" ? {
+ type: "none"
+ } : undefined;
+
+ return onUpdate({
+ credit_facade_credentials: creds,
+ credit_facade_url: state.credit_facade_url,
+ });
+ };
+
+ 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">
+ Account: <b>{account.id.substring(0, 8)}...</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ readonly
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ if (str === "basic") return "With authentication";
+ return "Do not change"
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
+
+function isValidURL(s: string): boolean {
+ try {
+ const u = new URL(s)
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
new file mode 100644
index 000000000..44dee7651
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -0,0 +1,96 @@
+/*
+ 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)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ bid: string;
+}
+export default function UpdateValidator({
+ bid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateBankAccount } = useBankAccountAPI();
+ const result = useBankAccountDetails(bid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ account={{ ...result.data, id: bid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateBankAccount(bid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..6b02430cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
@@ -0,0 +1,72 @@
+/*
+ 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/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+import emptyImage from "../../assets/empty.png";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Image</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <img src={entity.image} style={{ width: 200, height: 200 }} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Description</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <textarea class="input" readonly value={entity.description} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Price</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.price} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
new file mode 100644
index 000000000..4b59e9807
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
@@ -0,0 +1,60 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { AuditorBackend } from "../../../../declaration.js";
+import { useDepositConfirmationAPI } from "../../../../hooks/deposit_confirmations.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
+ const { createProduct } = useProductAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
+ return createProduct(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
new file mode 100644
index 000000000..c2c4d548c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Product/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
+ onDelete: { action: "onDelete" },
+ onUpdate: { action: "onUpdate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+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: {},
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
new file mode 100644
index 000000000..ffd1f12e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
@@ -0,0 +1,496 @@
+/*
+ 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)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { AuditorBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (depositConfirmation: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Deposit Confirmations</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add deposit-confirmation`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ ) => Promise<void>;
+ onDelete: (serial_id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ const restStockInfo = !i.next_restock
+ ? ""
+ : i.next_restock.t_s === "never"
+ ? "never"
+ : `restock at ${format(
+ new Date(i.next_restock.t_s * 1000),
+ dateFormatForSettings(settings),
+ )}`;
+ let stockInfo: ComponentChildren = "";
+ if (i.total_stock < 0) {
+ stockInfo = "infinite";
+ } else {
+ const totalStock = i.total_stock - i.total_lost - i.total_sold;
+ stockInfo = (
+ <label title={restStockInfo}>
+ {totalStock} {i.unit}
+ </label>
+ );
+ }
+
+ const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price));
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <img
+ src={i.image ? i.image : emptyImage}
+ style={{
+ border: "solid black 1px",
+ maxHeight: "2em",
+ width: "auto",
+ height: "auto",
+ }}
+ />
+ </td>
+ <td
+ class="has-tooltip-right"
+ data-tooltip={i.description}
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {sum(i.taxes)}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {difference(i.price, sum(i.taxes))}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {stockInfo}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <span style={{"whiteSpace":"nowrap"}}>
+
+ {i.total_sold} {i.unit}
+ </span>
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to product update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this product from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ {rowSelection === i.id && (
+ <tr key="form">
+ <td colSpan={10}>
+ <FastProductUpdateForm
+ product={i}
+ onUpdate={(prod) =>
+ onUpdate(i.id, prod).then((r) =>
+ rowSelectionHandler(undefined),
+ )
+ }
+ onCancel={() => rowSelectionHandler(undefined)}
+ />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdateFormProps {
+ product: Entity;
+ onUpdate: (
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCancel: () => void;
+}
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+function FastProductWithInfiniteStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<UpdatePrice>({ price: product.price });
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`update the product with new price`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-expanded">
+
+ <div class="buttons mt-5">
+
+ <button class="button mt-5" onClick={onCancel}>
+ <i18n.Translate>Clone</i18n.Translate>
+ </button>
+ </div>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`update product with new price`}
+ >
+ <button
+ class="button is-info"
+ onClick={() =>
+ onUpdate({
+ ...product,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductWithManagedStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<FastProductUpdate>({
+ incoming: 0,
+ lost: 0,
+ price: product.price,
+ });
+
+ const currentStock =
+ product.total_stock - product.total_sold - product.total_lost;
+
+ const errors: FormErrors<FastProductUpdate> = {
+ lost:
+ currentStock + value.incoming < value.lost
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputNumber<FastProductUpdate>
+ name="incoming"
+ label={i18n.str`Incoming`}
+ tooltip={i18n.str`add more elements to the inventory`}
+ />
+ <InputNumber<FastProductUpdate>
+ name="lost"
+ label={i18n.str`Lost`}
+ tooltip={i18n.str`report elements lost in the inventory`}
+ />
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`new price for the product`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ hasErrors
+ ? i18n.str`the are value with errors`
+ : i18n.str`update product with new stock and price`
+ }
+ >
+ <button
+ class="button is-info"
+ disabled={hasErrors}
+ onClick={() =>
+ onUpdate({
+ ...product,
+ total_stock: product.total_stock + value.incoming,
+ total_lost: product.total_lost + value.lost,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductUpdateForm(props: FastProductUpdateFormProps) {
+ return props.product.total_stock === -1 ? (
+ <FastProductWithInfiniteStockUpdateForm {...props} />
+ ) : (
+ <FastProductWithManagedStockUpdateForm {...props} />
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no products yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function difference(price: string, tax: number) {
+ if (!tax) return price;
+ const ps = price.split(":");
+ const p = parseInt(ps[1], 10);
+ ps[1] = `${p - tax}`;
+ return ps.join(":");
+}
+function sum(taxes: MerchantBackend.Tax[]) {
+ return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
new file mode 100644
index 000000000..dccb3ef25
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
@@ -0,0 +1,151 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { AuditorBackend, WithId } from "../../../../declaration.js";
+import {
+ useDepositConfirmation,
+ useDepositConfirmationAPI,
+} from "../../../../hooks/deposit_confirmations.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onLoadError: (e: HttpError<AuditorBackend.ErrorDetail>) => VNode;
+}
+export default function DepositConfirmationList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useDepositConfirmation();
+ const { deleteDepositConfirmation, updateDepositConfirmation, getDepositConfirmation } = useDepositConfirmationAPI();
+ const [deleting, setDeleting] =
+ useState<AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getDepositConfirmation}
+ onSelect={onSelect}
+ description={i18n.str`jump to deposit_confirmation with the given serial ID`}
+ palceholder={i18n.str`serial id`}
+ />
+
+ <CardTable
+ instances={result.data}
+ onCreate={onCreate}
+ onUpdate={(id, prod) =>
+ updateDepositConfirmation(id, prod)
+ .then(() =>
+ setNotif({
+ message: i18n.str`deposit_confirmation updated successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not update the deposit_confirmation`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ onSelect={(depositConfirmation) => onSelect(depositConfirmation.id)}
+ onDelete={(depositConfirmation : AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId) =>
+ setDeleting(depositConfirmation)
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete deposit-confirmation`}
+ description={`Delete the deposit-cofirmation "${deleting.serial_id}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteDepositConfirmation(deleting.serial_id);
+ setNotif({
+ message: i18n.str`Deposit-confirmation "${deleting.serial_id}" (ID: ${deleting.serial_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete deposit-confirmation`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the deposit-confirmation (ID:{" "}
+ <b>{deleting.serial_id}</b>), the stock and related information will be lost
+ </p>
+ <p class="warning">
+ Deleting a deposit-confirmation <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ 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 WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ 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>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
new file mode 100644
index 000000000..21dadb1e3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
@@ -0,0 +1,83 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { MerchantBackend } from "../../../declaration.js";
+
+type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage;
+interface Props {
+ onUpdate: () => void;
+ onDelete: () => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const defaults = {
+ default_wire_fee_amortization: 1,
+ use_stefan: true,
+ default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
+ default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
+ };
+ return { ...defaults, ...from };
+}
+
+export function DetailPage({ selected }: Props): VNode {
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">Here goes the instance description</h1>
+ </div>
+ </div>
+ <div class="level-right" style="display: none;">
+ <div class="level-item" />
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-6">
+ <FormProvider<Entity> object={value} valueHandler={valueHandler}>
+ <Input<Entity> name="name" readonly label={i18n.str`Name`} />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
new file mode 100644
index 000000000..9b393b818
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
@@ -0,0 +1,87 @@
+/*
+ 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/>
+ */
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { DeleteModal } from "../../../components/modal/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdate: () => void;
+ onNotFound: () => VNode;
+ onDelete: () => void;
+}
+
+export default function Detail({
+ onUpdate,
+ onLoadError,
+ onUnauthorized,
+ onDelete,
+ onNotFound,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails();
+ const [deleting, setDeleting] = useState<boolean>(false);
+
+ const { deleteInstance } = useInstanceAPI();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <DetailPage
+ selected={result.data}
+ onUpdate={onUpdate}
+ onDelete={() => setDeleting(true)}
+ />
+ {deleting && (
+ <DeleteModal
+ element={{ name: result.data.name, id }}
+ onCancel={() => setDeleting(false)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteInstance();
+ onDelete();
+ } catch (error) {
+ //FIXME: show message error
+ }
+ setDeleting(false);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
new file mode 100644
index 000000000..367fabce2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -0,0 +1,68 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ConfigContextProvider } from "../../../context/config.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Instance/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Internal: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const component = (args: any) => (
+ <ConfigContextProvider
+ value={{
+ currency: "TESTKUDOS",
+ version: "1",
+ }}
+ >
+ <Internal {...(props as any)} />
+ </ConfigContextProvider>
+ );
+ return { component, props };
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ jurisdiction: {},
+ use_stefan: true,
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
new file mode 100644
index 000000000..1d8c76ff9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+export * as details from "./details/stories.js";
+export * as kycList from "./kyc/list/ListPage.stories.js";
+export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
new file mode 100644
index 000000000..d33f64ada
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
@@ -0,0 +1,58 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export default {
+ title: "Pages/KYC/List",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const Example = tests.createExample(TestedComponent, {
+ status: {
+ timeout_kycs: [],
+ pending_kycs: [
+ {
+ aml_status: 0,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ kyc_url: "http://exchange.taler/kyc",
+ },
+ {
+ aml_status: 1,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ {
+ aml_status: 2,
+ exchange_url: "http://exchange.taler",
+ payto_uri: "payto://iban/de123123123",
+ },
+ ],
+ } as MerchantBackend.KYC.AccountKycRedirects,
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
new file mode 100644
index 000000000..338081886
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -0,0 +1,208 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+
+export interface Props {
+ status: MerchantBackend.KYC.AccountKycRedirects;
+}
+
+export function ListPage({ status }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <section class="section is-main-section">
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Pending KYC verification</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.pending_kycs.length > 0 ? (
+ <PendingTable entries={status.pending_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {status.timeout_kycs.length > 0 ? (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ <i18n.Translate>Timed out</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {status.timeout_kycs.length > 0 ? (
+ <TimedOutTable entries={status.timeout_kycs} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : undefined}
+ </section>
+ );
+}
+interface PendingTableProps {
+ entries: MerchantBackend.KYC.MerchantAccountKycRedirect[];
+}
+
+interface TimedOutTableProps {
+ entries: MerchantBackend.KYC.ExchangeKycTimeout[];
+}
+
+function PendingTable({ entries }: PendingTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Target account</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ if (e.kyc_url === undefined) {
+ // blocked by AML
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ {e.aml_status === 1 ? (
+ <i18n.Translate>
+ There is an anti-money laundering process pending to
+ complete.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ The account is frozen due to the anti-money laundering
+ rules. Contact the exchange service provider for further
+ instructions.
+ </i18n.Translate>
+ )}
+ </td>
+ </tr>
+ );
+ } else {
+ // blocked by KYC
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.payto_uri}</td>
+ <td>
+ <a href={e.kyc_url} target="_black" rel="noreferrer">
+ <i18n.Translate>
+ Pending KYC process, click here to complete
+ </i18n.Translate>
+ </a>
+ </td>
+ </tr>
+ );
+ }
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function TimedOutTable({ entries }: TimedOutTableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Exchange</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Code</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Http Status</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {entries.map((e, i) => {
+ return (
+ <tr key={i}>
+ <td>{e.exchange_url}</td>
+ <td>{e.exchange_code}</td>
+ <td>{e.exchange_http_status}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-happy mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>No pending kyc verification!</i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
new file mode 100644
index 000000000..5b93ac169
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -0,0 +1,63 @@
+/*
+ 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)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+}
+
+export default function ListKYC({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceKYCDetails();
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const status = result.data.type === "ok" ? undefined : result.data.status;
+
+ if (!status) {
+ return <div>no kyc required</div>;
+ }
+ return <ListPage status={status} />;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
new file mode 100644
index 000000000..bd9f65718
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
@@ -0,0 +1,71 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Order/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ instanceConfig: {
+ default_pay_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ use_stefan: true,
+ },
+ instanceInventory: [
+ {
+ id: "t-shirt-1",
+ description: "a m size t-shirt",
+ price: "TESTKUDOS:1",
+ total_stock: -1,
+ },
+ {
+ id: "t-shirt-2",
+ price: "TESTKUDOS:1",
+ description: "a xl size t-shirt",
+ } as any,
+ {
+ id: "t-shirt-3",
+ price: "TESTKUDOS:1",
+ description: "a s size t-shirt",
+ } as any,
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
new file mode 100644
index 000000000..fbfd023c1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -0,0 +1,705 @@
+/*
+ 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)
+ */
+
+import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, isFuture } from "date-fns";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
+import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { useSettings } from "../../../../hooks/useSettings.js";
+import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
+import { rate } from "../../../../utils/amount.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+interface Props {
+ onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
+ onBack?: () => void;
+ instanceConfig: InstanceConfig;
+ instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
+}
+interface InstanceConfig {
+ use_stefan: boolean;
+ default_pay_delay: TalerProtocolDuration;
+ default_wire_transfer_delay: TalerProtocolDuration;
+}
+
+function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
+ const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay);
+ const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay);
+
+ return {
+ inventoryProducts: {},
+ products: [],
+ pricing: {},
+ payments: {
+ max_fee: undefined,
+ createToken: true,
+ pay_deadline: (defaultPayDeadline),
+ refund_deadline: (defaultPayDeadline),
+ wire_transfer_deadline: (defaultWireDeadline),
+ },
+ shipping: {},
+ extra: {},
+ };
+}
+
+interface ProductAndQuantity {
+ product: MerchantBackend.Products.ProductDetail & WithId;
+ quantity: number;
+}
+export interface ProductMap {
+ [id: string]: ProductAndQuantity;
+}
+
+interface Pricing {
+ products_price: string;
+ order_price: string;
+ summary: string;
+}
+interface Shipping {
+ delivery_date?: Date;
+ delivery_location?: MerchantBackend.Location;
+ fullfilment_url?: string;
+}
+interface Payments {
+ refund_deadline: Duration;
+ pay_deadline: Duration;
+ wire_transfer_deadline: Duration;
+ auto_refund_deadline: Duration;
+ max_fee?: string;
+ createToken: boolean;
+ minimum_age?: number;
+}
+interface Entity {
+ inventoryProducts: ProductMap;
+ products: MerchantBackend.Product[];
+ pricing: Partial<Pricing>;
+ payments: Partial<Payments>;
+ shipping: Partial<Shipping>;
+ extra: Record<string, string>;
+}
+
+const stringIsValidJSON = (value: string) => {
+ try {
+ JSON.parse(value.trim());
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export function CreatePage({
+ onCreate,
+ onBack,
+ instanceConfig,
+ instanceInventory,
+}: Props): VNode {
+ const config = useConfigContext();
+ const instance_default = with_defaults(instanceConfig, config.currency)
+ const [value, valueHandler] = useState(instance_default);
+ const zero = Amounts.zeroOfCurrency(config.currency);
+ const [settings, updateSettings] = useSettings()
+ const inventoryList = Object.values(value.inventoryProducts || {});
+ const productList = Object.values(value.products || {});
+
+ const { i18n } = useTranslationContext();
+
+ const parsedPrice = !value.pricing?.order_price
+ ? undefined
+ : Amounts.parse(value.pricing.order_price);
+
+ const errors: FormErrors<Entity> = {
+ pricing: undefinedIfEmpty({
+ summary: !value.pricing?.summary ? i18n.str`required` : undefined,
+ order_price: !value.pricing?.order_price
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ }),
+ payments: undefinedIfEmpty({
+ refund_deadline: !value.payments?.refund_deadline
+ ? undefined
+ : value.payments.pay_deadline &&
+ Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1
+ ? i18n.str`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before refund deadline`
+ : undefined,
+ pay_deadline: !value.payments?.pay_deadline
+ ? i18n.str`required`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline,
+ ) === -1
+ ? i18n.str`wire transfer deadline cannot be before pay deadline`
+ : undefined,
+ wire_transfer_deadline: !value.payments?.wire_transfer_deadline
+ ? i18n.str`required`
+ : undefined,
+ auto_refund_deadline: !value.payments?.auto_refund_deadline
+ ? undefined
+ : !value.payments?.refund_deadline
+ ? i18n.str`should have a refund deadline`
+ : Duration.cmp(
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline,
+ ) == -1
+ ? i18n.str`auto refund cannot be after refund deadline`
+ : undefined,
+
+ }),
+ shipping: undefinedIfEmpty({
+ delivery_date: !value.shipping?.delivery_date
+ ? undefined
+ : !isFuture(value.shipping.delivery_date)
+ ? i18n.str`should be in the future`
+ : undefined,
+ }),
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = (): void => {
+ const order = value as any; //schema.cast(value);
+ if (!value.payments) return;
+ if (!value.shipping) return;
+
+ const request: MerchantBackend.Orders.PostOrderRequest = {
+ order: {
+ amount: order.pricing.order_price,
+ summary: order.pricing.summary,
+ products: productList,
+ extra: undefinedIfEmpty(value.extra),
+ pay_deadline: !value.payments.pay_deadline ?
+ i18n.str`required` :
+ AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline))
+ ,// : undefined,
+ wire_transfer_deadline: value.payments.wire_transfer_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline))
+ : undefined,
+ refund_deadline: value.payments.refund_deadline
+ ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline))
+ : undefined,
+ auto_refund: value.payments.auto_refund_deadline
+ ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline)
+ : undefined,
+ max_fee: value.payments.max_fee as string,
+
+ delivery_date: value.shipping.delivery_date
+ ? { t_s: value.shipping.delivery_date.getTime() / 1000 }
+ : undefined,
+ delivery_location: value.shipping.delivery_location,
+ fulfillment_url: value.shipping.fullfilment_url,
+ minimum_age: value.payments.minimum_age,
+ },
+ inventory_products: inventoryList.map((p) => ({
+ product_id: p.product.id,
+ quantity: p.quantity,
+ })),
+ create_token: value.payments.createToken,
+ };
+
+ onCreate(request);
+ };
+
+ const addProductToTheInventoryList = (
+ product: MerchantBackend.Products.ProductDetail & WithId,
+ quantity: number,
+ ) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ inventoryProducts[product.id] = { product, quantity };
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const removeProductFromTheInventoryList = (id: string) => {
+ valueHandler((v) => {
+ const inventoryProducts = { ...v.inventoryProducts };
+ delete inventoryProducts[id];
+ return { ...v, inventoryProducts };
+ });
+ };
+
+ const addNewProduct = async (product: MerchantBackend.Product) => {
+ return valueHandler((v) => {
+ const products = v.products ? [...v.products, product] : [];
+ return { ...v, products };
+ });
+ };
+
+ const removeFromNewProduct = (index: number) => {
+ valueHandler((v) => {
+ const products = v.products ? [...v.products] : [];
+ products.splice(index, 1);
+ return { ...v, products };
+ });
+ };
+
+ const [editingProduct, setEditingProduct] = useState<
+ MerchantBackend.Product | undefined
+ >(undefined);
+
+ 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 totalPriceProducts = productList.reduce((prev, cur) => {
+ if (!cur.price) return zero;
+ 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 totalAsString = Amounts.stringify(totalPrice.amount);
+ const allProducts = productList.concat(inventoryList.map(asProduct));
+
+ const [newField, setNewField] = useState("")
+
+ useEffect(() => {
+ valueHandler((v) => {
+ return {
+ ...v,
+ pricing: {
+ ...v.pricing,
+ products_price: hasProducts ? totalAsString : undefined,
+ order_price: hasProducts ? totalAsString : undefined,
+ },
+ };
+ });
+ }, [hasProducts, totalAsString]);
+
+ const discountOrRise = rate(
+ parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
+ totalPrice.amount,
+ );
+
+ const minAgeByProducts = allProducts.reduce(
+ (cur, prev) =>
+ !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age,
+ 0,
+ );
+
+ // if there is no default pay deadline
+ const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline
+ // and there is no defailt wire deadline
+ const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline
+ // user required to set the taler options
+ const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline
+
+
+ return (
+ <div>
+
+ <section class="section is-main-section">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
+ updateSettings({
+ ...settings,
+ advanceOrderMode: false
+ })
+ }}>
+ <a >
+ <span><i18n.Translate>Simple</i18n.Translate></span>
+ </a>
+ </li>
+ <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
+ updateSettings({
+ ...settings,
+ advanceOrderMode: true
+ })
+ }}>
+ <a >
+ <span><i18n.Translate>Advanced</i18n.Translate></span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ {/* // FIXME: translating plural singular */}
+ <InputGroup
+ name="inventory_products"
+ label={i18n.str`Manage products in order`}
+ alternative={
+ allProducts.length > 0 && (
+ <p>
+ {allProducts.length} products with a total price of{" "}
+ {totalAsString}.
+ </p>
+ )
+ }
+ tooltip={i18n.str`Manage list of products in the order.`}
+ >
+ <InventoryProductForm
+ currentProducts={value.inventoryProducts || {}}
+ onAddProduct={addProductToTheInventoryList}
+ inventory={instanceInventory}
+ />
+
+ {settings.advanceOrderMode &&
+ <NonInventoryProductFrom
+ productToEdit={editingProduct}
+ onAddProduct={(p) => {
+ setEditingProduct(undefined);
+ return addNewProduct(p);
+ }}
+ />
+ }
+
+ {allProducts.length > 0 && (
+ <ProductList
+ list={allProducts}
+ actions={[
+ {
+ name: i18n.str`Remove`,
+ tooltip: i18n.str`Remove this product from the order.`,
+ handler: (e, index) => {
+ if (e.product_id) {
+ removeProductFromTheInventoryList(e.product_id);
+ } else {
+ removeFromNewProduct(index);
+ setEditingProduct(e);
+ }
+ },
+ },
+ ]}
+ />
+ )}
+ </InputGroup>
+
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ {hasProducts ? (
+ <Fragment>
+ <InputCurrency
+ name="pricing.products_price"
+ label={i18n.str`Total price`}
+ readonly
+ tooltip={i18n.str`total product price added up`}
+ />
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n.str`Total price`}
+ addonAfter={
+ discountOrRise > 0 &&
+ (discountOrRise < 1
+ ? `discount of %${Math.round(
+ (1 - discountOrRise) * 100,
+ )}`
+ : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
+ }
+ tooltip={i18n.str`Amount to be paid by the customer`}
+ />
+ </Fragment>
+ ) : (
+ <InputCurrency
+ name="pricing.order_price"
+ label={i18n.str`Order price`}
+ tooltip={i18n.str`final order price`}
+ />
+ )}
+
+ <Input
+ name="pricing.summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="shipping"
+ label={i18n.str`Shipping and Fulfillment`}
+ initialActive
+ >
+ <InputDate
+ name="shipping.delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
+ />
+ {value.shipping?.delivery_date && (
+ <InputGroup
+ name="shipping.delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`address where the products will be delivered`}
+ >
+ <InputLocation name="shipping.delivery_location" />
+ </InputGroup>
+ )}
+ <Input
+ name="shipping.fullfilment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
+ />
+ </InputGroup>
+ }
+
+ {(settings.advanceOrderMode || requiresSomeTalerOptions) &&
+ <InputGroup
+ name="payments"
+ label={i18n.str`Taler payment options`}
+ tooltip={i18n.str`Override default Taler payment settings for this order`}
+ >
+ {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration
+ name="payments.pay_deadline"
+ label={i18n.str`Payment time`}
+ help={<DeadlineHelp duration={value.payments?.pay_deadline} />}
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ const c = {
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ pay_deadline: instance_default.payments?.pay_deadline
+ }
+ }
+ valueHandler(c)
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {settings.advanceOrderMode && <InputDuration
+ name="payments.refund_deadline"
+ label={i18n.str`Refund time`}
+ help={<DeadlineHelp duration={value.payments?.refund_deadline} />}
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ refund_deadline: instance_default.payments?.refund_deadline
+ }
+ })
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration
+ name="payments.wire_transfer_deadline"
+ label={i18n.str`Wire transfer time`}
+ help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />}
+ withoutClear
+ withForever
+ tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
+ side={
+ <span>
+ <button class="button" onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline
+ }
+ })
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />}
+ {settings.advanceOrderMode && <InputDuration
+ name="payments.auto_refund_deadline"
+ label={i18n.str`Auto-refund time`}
+ help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />}
+ tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
+ withForever
+ />}
+
+ {settings.advanceOrderMode && <InputCurrency
+ name="payments.max_fee"
+ label={i18n.str`Maximum fee`}
+ tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
+ />}
+ {settings.advanceOrderMode && <InputToggle
+ name="payments.createToken"
+ label={i18n.str`Create token`}
+ tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
+ />}
+ {settings.advanceOrderMode && <InputNumber
+ name="payments.minimum_age"
+ label={i18n.str`Minimum age required`}
+ tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
+ help={
+ minAgeByProducts > 0
+ ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
+ : i18n.str`No product with age restriction in this order`
+ }
+ />}
+ </InputGroup>
+ }
+
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="extra"
+ label={i18n.str`Additional information`}
+ tooltip={i18n.str`Custom information to be included in the contract for this order.`}
+ >
+ {Object.keys(value.extra ?? {}).map((key) => {
+
+ return <Input
+ name={`extra.${key}`}
+ inputType="multiline"
+ label={key}
+ tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
+ side={
+ <button class="button" onClick={(e) => {
+ if (value.extra && value.extra[key] !== undefined) {
+ console.log(value.extra)
+ delete value.extra[key]
+ }
+ valueHandler({
+ ...value,
+ })
+ }}>remove</button>
+ }
+ />
+ })}
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Custom field name</i18n.Translate>
+ <span class="icon has-tooltip-right" data-tooltip={"new extra field"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} />
+ </p>
+ </div>
+ </div>
+ <button class="button" onClick={(e) => {
+ setNewField("")
+ valueHandler({
+ ...value,
+ extra: {
+ ...(value.extra ?? {}),
+ [newField]: ""
+ }
+ })
+ }}>add</button>
+ </div>
+ </InputGroup>
+ }
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <button
+ class="button is-success"
+ onClick={submit}
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
+ return {
+ product_id: p.product.id,
+ image: p.product.image,
+ price: p.product.price,
+ unit: p.product.unit,
+ quantity: p.quantity,
+ description: p.product.description,
+ taxes: p.product.taxes,
+ minimum_age: p.product.minimum_age,
+ };
+}
+
+
+function DeadlineHelp({ duration }: { duration?: Duration }): VNode {
+ const { i18n } = useTranslationContext();
+ const [now, setNow] = useState(AbsoluteTime.now())
+ useEffect(() => {
+ const iid = setInterval(() => {
+ setNow(AbsoluteTime.now())
+ }, 60 * 1000)
+ return () => {
+ clearInterval(iid)
+ }
+ })
+ if (!duration) return <i18n.Translate>Disabled</i18n.Translate>
+ const when = AbsoluteTime.addDuration(now, duration)
+ if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate>
+ return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate>
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
new file mode 100644
index 000000000..88a984c97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -0,0 +1,114 @@
+/*
+ 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/>
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { Entity } from "./index.js";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function OrderCreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { getPaymentURL } = useOrderAPI();
+ const [url, setURL] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ getPaymentURL(entity.response.order_id).then((response) => {
+ setURL(response.data);
+ });
+ }, [getPaymentURL, entity.response.order_id]);
+
+ return (
+ <CreatedSuccessfully
+ onConfirm={onConfirm}
+ onCreateAnother={onCreateAnother}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Amount</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.amount}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Summary</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.request.order.summary}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Order ID</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.response.order_id} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Payment URL</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={url} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </CreatedSuccessfully>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
new file mode 100644
index 000000000..2474fd042
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -0,0 +1,114 @@
+/*
+ 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)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useOrderAPI } from "../../../../hooks/order.js";
+import { useInstanceProducts } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = {
+ request: MerchantBackend.Orders.PostOrderRequest;
+ response: MerchantBackend.Orders.PostOrderResponse;
+};
+interface Props {
+ onBack?: () => void;
+ onConfirm: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function OrderCreate({
+ onConfirm,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrder } = useOrderAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const detailsResult = useInstanceDetails();
+ const inventoryResult = useInstanceProducts();
+
+ if (detailsResult.loading) return <Loading />;
+ if (inventoryResult.loading) return <Loading />;
+
+ if (!detailsResult.ok) {
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ detailsResult.type === ErrorType.CLIENT &&
+ detailsResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(detailsResult);
+ }
+
+ if (!inventoryResult.ok) {
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ inventoryResult.type === ErrorType.CLIENT &&
+ inventoryResult.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(inventoryResult);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
+ createOrder(request)
+ .then((r) => {
+ return onConfirm(r.data.order_id)
+ })
+ .catch((error) => {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ instanceConfig={detailsResult.data}
+ instanceInventory={inventoryResult.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
new file mode 100644
index 000000000..6e73a01a5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
@@ -0,0 +1,135 @@
+/*
+ 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)
+ */
+
+import { addDays } from "date-fns";
+import { h, VNode, FunctionalComponent } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Order/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onRefund: { action: "onRefund" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+const defaultContractTerm = {
+ amount: "TESTKUDOS:10",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ auditors: [],
+ exchanges: [],
+ max_fee: "TESTKUDOS:1",
+ merchant: {} as any,
+ merchant_base_url: "http://merchant.url/",
+ order_id: "2021.165-03GDFC26Y1NNG",
+ products: [],
+ summary: "text summary",
+ wire_transfer_deadline: {
+ t_s: "never",
+ },
+ refund_deadline: { t_s: "never" },
+ merchant_pub: "ASDASDASDSd",
+ nonce: "QWEQWEQWE",
+ pay_deadline: {
+ t_s: "never",
+ },
+ wire_method: "x-taler-bank",
+ h_wire: "asd",
+} as MerchantBackend.ContractTerms;
+
+// contract_terms: defaultContracTerm,
+export const Claimed = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "claimed",
+ contract_terms: defaultContractTerm,
+ },
+});
+
+export const PaidNotRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: defaultContractTerm,
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const PaidRefundable = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "paid",
+ contract_terms: {
+ ...defaultContractTerm,
+ refund_deadline: {
+ t_s: addDays(new Date(), 2).getTime() / 1000,
+ },
+ },
+ refunded: false,
+ deposit_total: "TESTKUDOS:10",
+ exchange_ec: 0,
+ order_status_url: "http://merchant.backend/status",
+ exchange_hc: 0,
+ refund_amount: "TESTKUDOS:0",
+ refund_details: [],
+ refund_pending: false,
+ wire_details: [],
+ wire_reports: [],
+ wired: false,
+ },
+});
+
+export const Unpaid = createExample(TestedComponent, {
+ id: "2021.165-03GDFC26Y1NNG",
+ selected: {
+ order_status: "unpaid",
+ order_status_url: "http://merchant.backend/status",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ summary: "text summary",
+ taler_pay_uri: "pay uri",
+ total_amount: "TESTKUDOS:10",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
new file mode 100644
index 000000000..5ff76e37a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -0,0 +1,770 @@
+/*
+ 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)
+ */
+
+import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format, formatDistance } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputLocation } from "../../../../components/form/InputLocation.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { ProductList } from "../../../../components/product/ProductList.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { RefundModal } from "../list/Table.js";
+import { Event, Timeline } from "./Timeline.js";
+
+type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
+type CT = MerchantBackend.ContractTerms;
+
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+ id: string;
+ onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & {
+ refund_taken: string;
+};
+type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse;
+
+function ContractTerms({ value }: { value: CT }) {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InputGroup name="contract_terms" label={i18n.str`Contract Terms`}>
+ <FormProvider<CT> object={value} valueHandler={null}>
+ <Input<CT>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<CT>
+ readonly
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ {value.fulfillment_url && (
+ <Input<CT>
+ readonly
+ name="fulfillment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL for this purchase`}
+ />
+ )}
+ <Input<CT>
+ readonly
+ name="max_fee"
+ label={i18n.str`Max fee`}
+ tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="timestamp"
+ label={i18n.str`Created at`}
+ tooltip={i18n.str`time when this contract was generated`}
+ />
+ <InputDate<CT>
+ readonly
+ name="refund_deadline"
+ label={i18n.str`Refund deadline`}
+ tooltip={i18n.str`after this deadline has passed no refunds will be accepted`}
+ />
+ <InputDate<CT>
+ readonly
+ name="pay_deadline"
+ label={i18n.str`Payment deadline`}
+ tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`}
+ />
+ <InputDate<CT>
+ readonly
+ name="wire_transfer_deadline"
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`transfer deadline for the exchange`}
+ />
+ <InputDate<CT>
+ readonly
+ name="delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`time indicating when the order should be delivered`}
+ />
+ {value.delivery_date && (
+ <InputGroup
+ name="delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`where the order will be delivered`}
+ >
+ <InputLocation name="payments.delivery_location" />
+ </InputGroup>
+ )}
+ <InputDuration<CT>
+ readonly
+ name="auto_refund"
+ label={i18n.str`Auto-refund delay`}
+ tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`}
+ />
+ <Input<CT>
+ readonly
+ name="extra"
+ label={i18n.str`Extra info`}
+ tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`}
+ />
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
+function ClaimedPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentClaimedResponse;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+
+ const [value, valueHandler] = useState<Partial<Claimed>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-info ml-4">
+ <i18n.Translate>claimed</i18n.Translate>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>claimed at</i18n.Translate>:
+ </b>{" "}
+ {format(
+ new Date(order.contract_terms.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Claimed>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <Input
+ name="contract_terms.summary"
+ readonly
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ />
+ <InputCurrency
+ name="contract_terms.amount"
+ readonly
+ label={i18n.str`Amount`}
+ />
+ <Input<Claimed>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+function PaidPage({
+ id,
+ order,
+ onRefund,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentPaidResponse;
+ onRefund: (id: string) => void;
+}) {
+ const events: Event[] = [];
+ if (order.contract_terms.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.timestamp.t_s * 1000),
+ description: "order created",
+ type: "start",
+ });
+ }
+ if (order.contract_terms.pay_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
+ description: "pay deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.refund_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
+ description: "refund deadline",
+ type: "deadline",
+ });
+ }
+ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ events.push({
+ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ description: "wire deadline",
+ type: "deadline",
+ });
+ }
+ if (
+ order.contract_terms.delivery_date &&
+ order.contract_terms.delivery_date.t_s !== "never"
+ ) {
+ if (order.contract_terms.delivery_date)
+ events.push({
+ when: new Date(order.contract_terms.delivery_date?.t_s * 1000),
+ description: "delivery",
+ type: "delivery",
+ });
+ }
+ order.refund_details.reduce(mergeRefunds, []).forEach((e) => {
+ if (e.timestamp.t_s !== "never") {
+ events.push({
+ when: new Date(e.timestamp.t_s * 1000),
+ description: `refund: ${e.amount}: ${e.reason}`,
+ type: e.pending ? "refund" : "refund-taken",
+ });
+ }
+ });
+ if (order.wire_details && order.wire_details.length) {
+ if (order.wire_details.length > 1) {
+ let last: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let first: MerchantBackend.Orders.TransactionWireTransfer | null = null;
+ let total: AmountJson | null = null;
+
+ order.wire_details.forEach((w) => {
+ if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
+ last = w;
+ }
+ if (first === null || first.execution_time.t_s > w.execution_time.t_s) {
+ first = w;
+ }
+ total =
+ total === null
+ ? Amounts.parseOrThrow(w.amount)
+ : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
+ });
+ const last_time = last!.execution_time.t_s;
+ if (last_time !== "never") {
+ events.push({
+ when: new Date(last_time * 1000),
+ description: `wired ${Amounts.stringify(total!)}`,
+ type: "wired-range",
+ });
+ }
+ const first_time = first!.execution_time.t_s;
+ if (first_time !== "never") {
+ events.push({
+ when: new Date(first_time * 1000),
+ description: `wire transfer started...`,
+ type: "wired-range",
+ });
+ }
+ } else {
+ order.wire_details.forEach((e) => {
+ if (e.execution_time.t_s !== "never") {
+ events.push({
+ when: new Date(e.execution_time.t_s * 1000),
+ description: `wired ${e.amount}`,
+ type: "wired",
+ });
+ }
+ });
+ }
+ }
+
+ const now = new Date()
+ const nextEvent = events.find((e) => {
+ return e.when.getTime() > now.getTime()
+ })
+
+ const [value, valueHandler] = useState<Partial<Paid>>(order);
+ const { url: backendURL } = useBackendContext()
+ const refundurl = stringifyRefundUri({
+ merchantBaseUrl: backendURL,
+ orderId: order.contract_terms.order_id
+ })
+ const refundable =
+ new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
+ const { i18n } = useTranslationContext();
+
+ const amount = Amounts.parseOrThrow(order.contract_terms.amount);
+ const refund_taken = order.refund_details.reduce((prev, cur) => {
+ if (cur.pending) return prev;
+ return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount;
+ }, Amounts.zeroOfCurrency(amount.currency));
+ value.refund_taken = Amounts.stringify(refund_taken);
+
+ return (
+ <div>
+ <section class="section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ <div class="tag is-success ml-4">
+ <i18n.Translate>paid</i18n.Translate>
+ </div>
+ {order.wired ? (
+ <div class="tag is-success ml-4">
+ <i18n.Translate>wired</i18n.Translate>
+ </div>
+ ) : null}
+ {order.refunded ? (
+ <div class="tag is-danger ml-4">
+ <i18n.Translate>refunded</i18n.Translate>
+ </div>
+ ) : null}
+ </div>
+ </div>
+ </div>
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">{order.contract_terms.amount}</h1>
+ </div>
+ </div>
+ <div class="level-right">
+ <div class="level-item">
+ <h1 class="title">
+ <div class="buttons">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ refundable
+ ? i18n.str`refund order`
+ : i18n.str`not refundable`
+ }
+ >
+ <button
+ class="button is-danger"
+ disabled={!refundable}
+ onClick={() => onRefund(id)}
+ >
+ <i18n.Translate>refund</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ {nextEvent &&
+ <p>
+ <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
+ nextEvent.when,
+ new Date(),
+ // "yyyy/MM/dd HH:mm:ss",
+ )}
+ </p>
+ }
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="columns">
+ <div class="column is-4">
+ <div class="title">
+ <i18n.Translate>Timeline</i18n.Translate>
+ </div>
+ <Timeline events={events} />
+ </div>
+ <div class="column is-8">
+ <div class="title">
+ <i18n.Translate>Payment details</i18n.Translate>
+ </div>
+ <FormProvider<Paid>
+ object={value}
+ valueHandler={valueHandler}
+ >
+ {/* <InputCurrency<Paid> name="deposit_total" readonly label={i18n.str`Deposit total`} /> */}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_amount"
+ readonly
+ label={i18n.str`Refunded amount`}
+ />
+ )}
+ {order.refunded && (
+ <InputCurrency<Paid>
+ name="refund_taken"
+ readonly
+ label={i18n.str`Refund taken`}
+ />
+ )}
+ <Input<Paid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Status URL`}
+ >
+ <a
+ target="_blank"
+ rel="noreferrer"
+ href={order.order_status_url}
+ >
+ {order.order_status_url}
+ </a>
+ </TextField>
+ {order.refunded && (
+ <TextField<Paid>
+ name="order_status_url"
+ label={i18n.str`Refund URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={refundurl}>
+ {refundurl}
+ </a>
+ </TextField>
+ )}
+ </FormProvider>
+ </div>
+ </div>
+ </section>
+
+ {order.contract_terms.products.length ? (
+ <Fragment>
+ <div class="title">
+ <i18n.Translate>Product list</i18n.Translate>
+ </div>
+ <ProductList list={order.contract_terms.products} />
+ </Fragment>
+ ) : undefined}
+
+ {value.contract_terms && (
+ <ContractTerms value={value.contract_terms} />
+ )}
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+function UnpaidPage({
+ id,
+ order,
+}: {
+ id: string;
+ order: MerchantBackend.Orders.CheckPaymentUnpaidResponse;
+}) {
+ const [value, valueHandler] = useState<Partial<Unpaid>>(order);
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
+ return (
+ <div>
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <h1 class="title">
+ <i18n.Translate>Order</i18n.Translate> #{id}
+ </h1>
+ </div>
+ <div class="tag is-dark">
+ <i18n.Translate>unpaid</i18n.Translate>
+ </div>
+ </div>
+ </div>
+
+ <div class="level">
+ <div class="level-left" style={{ maxWidth: "100%" }}>
+ <div class="level-item" style={{ maxWidth: "100%" }}>
+ <div
+ class="content"
+ style={{
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <p>
+ <b>
+ <i18n.Translate>pay at</i18n.Translate>:
+ </b>{" "}
+ <a
+ href={order.order_status_url}
+ rel="nofollow"
+ target="new"
+ >
+ {order.order_status_url}
+ </a>
+ </p>
+ <p>
+ <b>
+ <i18n.Translate>created at</i18n.Translate>:
+ </b>{" "}
+ {order.creation_time.t_s === "never"
+ ? "never"
+ : format(
+ new Date(order.creation_time.t_s * 1000),
+ datetimeFormatForSettings(settings)
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Unpaid> object={value} valueHandler={valueHandler}>
+ <Input<Unpaid>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`human-readable description of the whole purchase`}
+ />
+ <InputCurrency<Unpaid>
+ readonly
+ name="total_amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`total price for the transaction`}
+ />
+ <Input<Unpaid>
+ name="order_status"
+ readonly
+ label={i18n.str`Order status`}
+ />
+ <Input<Unpaid>
+ name="order_status_url"
+ readonly
+ label={i18n.str`Order status URL`}
+ />
+ <TextField<Unpaid>
+ name="taler_pay_uri"
+ label={i18n.str`Payment URI`}
+ >
+ <a target="_blank" rel="noreferrer" href={value.taler_pay_uri}>
+ {value.taler_pay_uri}
+ </a>
+ </TextField>
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
+
+export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
+ const [showRefund, setShowRefund] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const DetailByStatus = function () {
+ switch (selected.order_status) {
+ case "claimed":
+ return <ClaimedPage id={id} order={selected} />;
+ case "paid":
+ return <PaidPage id={id} order={selected} onRefund={setShowRefund} />;
+ case "unpaid":
+ return <UnpaidPage id={id} order={selected} />;
+ default:
+ return (
+ <div>
+ <i18n.Translate>
+ Unknown order status. This is an error, please contact the
+ administrator.
+ </i18n.Translate>
+ </div>
+ );
+ }
+ };
+
+ return (
+ <Fragment>
+ {DetailByStatus()}
+ {showRefund && (
+ <RefundModal
+ order={selected}
+ onCancel={() => setShowRefund(undefined)}
+ onConfirm={(value) => {
+ onRefund(showRefund, value);
+ setShowRefund(undefined);
+ }}
+ />
+ )}
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </Fragment>
+ );
+}
+
+async function copyToClipboard(text: string) {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
new file mode 100644
index 000000000..8c863f386
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
@@ -0,0 +1,129 @@
+/*
+ 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/>
+ */
+import { format } from "date-fns";
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+interface Props {
+ events: Event[];
+}
+
+export function Timeline({ events: e }: Props) {
+ const events = [...e];
+ events.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+
+ events.sort((a, b) => a.when.getTime() - b.when.getTime());
+ const [settings] = useSettings();
+ const [state, setState] = useState(events);
+ useEffect(() => {
+ const handle = setTimeout(() => {
+ const eventsWithoutNow = state.filter((e) => e.type !== "now");
+ eventsWithoutNow.push({
+ when: new Date(),
+ description: "now",
+ type: "now",
+ });
+ setState(eventsWithoutNow);
+ }, 1000);
+ return () => {
+ clearTimeout(handle);
+ };
+ });
+ return (
+ <div class="timeline">
+ {events.map((e, i) => {
+ return (
+ <div key={i} class="timeline-item">
+ {(() => {
+ switch (e.type) {
+ case "deadline":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-flag" />
+ </div>
+ );
+ case "delivery":
+ return (
+ <div class="timeline-marker is-icon ">
+ <i class="mdi mdi-delivery" />
+ </div>
+ );
+ case "start":
+ return (
+ <div class="timeline-marker is-icon">
+ <i class="mdi mdi-flag " />
+ </div>
+ );
+ case "wired":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "wired-range":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund":
+ return (
+ <div class="timeline-marker is-icon is-danger">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "refund-taken":
+ return (
+ <div class="timeline-marker is-icon is-success">
+ <i class="mdi mdi-cash" />
+ </div>
+ );
+ case "now":
+ return (
+ <div class="timeline-marker is-icon is-info">
+ <i class="mdi mdi-clock" />
+ </div>
+ );
+ }
+ })()}
+ <div class="timeline-content">
+ {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
+ <p>{e.description}</p>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+export interface Event {
+ when: Date;
+ description: string;
+ type:
+ | "start"
+ | "refund"
+ | "refund-taken"
+ | "wired"
+ | "wired-range"
+ | "deadline"
+ | "delivery"
+ | "now";
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
new file mode 100644
index 000000000..1517a3c42
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -0,0 +1,95 @@
+/*
+ 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/>
+ */
+import {
+ useTranslationContext,
+ HttpError,
+ ErrorType,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export interface Props {
+ oid: string;
+
+ onBack: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+
+export default function Update({
+ oid,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { refundOrder } = useOrderAPI();
+ const result = useOrderDetails(oid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <DetailPage
+ onBack={onBack}
+ id={oid}
+ onRefund={(id, value) =>
+ refundOrder(id, value)
+ .then(() =>
+ setNotif({
+ message: i18n.str`refund created successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ selected={result.data}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
new file mode 100644
index 000000000..156c577f4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
@@ -0,0 +1,107 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Order/List",
+ component: TestedComponent,
+ argTypes: {
+ onShowAll: { action: "onShowAll" },
+ onShowPaid: { action: "onShowPaid" },
+ onShowRefunded: { action: "onShowRefunded" },
+ onShowNotWired: { action: "onShowNotWired" },
+ onCopyURL: { action: "onCopyURL" },
+ onSelectDate: { action: "onSelectDate" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onSelectOrder: { action: "onSelectOrder" },
+ onRefundOrder: { action: "onRefundOrder" },
+ onSearchOrderById: { action: "onSearchOrderById" },
+ onCreate: { action: "onCreate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ orders: [
+ {
+ id: "123",
+ amount: "TESTKUDOS:10",
+ paid: false,
+ refundable: true,
+ row_id: 1,
+ summary: "summary",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "123",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: true,
+ refundable: true,
+ row_id: 2,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ {
+ id: "456",
+ amount: "TESTKUDOS:1",
+ paid: false,
+ refundable: false,
+ row_id: 3,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "456",
+ },
+ {
+ id: "234",
+ amount: "TESTKUDOS:12",
+ paid: false,
+ refundable: false,
+ row_id: 4,
+ summary:
+ "summary with long text, very very long text that someone want to add as a description of the order",
+ timestamp: {
+ t_s: new Date().getTime() / 1000,
+ },
+ order_id: "234",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
new file mode 100644
index 000000000..9f80719a1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -0,0 +1,226 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode, Fragment } from "preact";
+import { useState } from "preact/hooks";
+import { DatePicker } from "../../../../components/picker/DatePicker.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+export interface ListPageProps {
+ onShowAll: () => void;
+ onShowNotPaid: () => void;
+ onShowPaid: () => void;
+ onShowRefunded: () => void;
+ onShowNotWired: () => void;
+ onShowWired: () => void;
+ onCopyURL: (id: string) => void;
+ isAllActive: string;
+ isPaidActive: string;
+ isNotPaidActive: string;
+ isRefundedActive: string;
+ isNotWiredActive: string;
+ isWiredActive: string;
+
+ jumpToDate?: Date;
+ onSelectDate: (date?: Date) => void;
+
+ orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+
+ onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
+ onCreate: () => void;
+}
+
+export function ListPage({
+ hasMoreAfter,
+ hasMoreBefore,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ orders,
+ isAllActive,
+ onSelectOrder,
+ onRefundOrder,
+ jumpToDate,
+ onCopyURL,
+ onShowAll,
+ onShowPaid,
+ onShowNotPaid,
+ onShowRefunded,
+ onShowNotWired,
+ onShowWired,
+ onSelectDate,
+ isPaidActive,
+ isRefundedActive,
+ isNotWiredActive,
+ onCreate,
+ isNotPaidActive,
+ isWiredActive,
+}: ListPageProps): VNode {
+ const { i18n } = useTranslationContext();
+ const dateTooltip = i18n.str`select date to show nearby orders`;
+ const [pickDate, setPickDate] = useState(false);
+ const [settings] = useSettings();
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column is-two-thirds">
+ <div class="tabs" style={{ overflow: "inherit" }}>
+ <ul>
+ <li class={isNotPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowNotPaid}>
+ <i18n.Translate>New</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isPaidActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show paid orders`}
+ >
+ <a onClick={onShowPaid}>
+ <i18n.Translate>Paid</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isRefundedActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show orders with refunds`}
+ >
+ <a onClick={onShowRefunded}>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNotWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowNotWired}>
+ <i18n.Translate>Not wired</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowWired}>
+ <i18n.Translate>Completed</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isAllActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="column ">
+ <div class="buttons is-right">
+ <div class="field has-addons">
+ {jumpToDate && (
+ <div class="control">
+ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
+ <span
+ class="icon"
+ data-tooltip={i18n.str`clear date filter`}
+ >
+ <i class="mdi mdi-close" />
+ </span>
+ </a>
+ </div>
+ )}
+ <div class="control">
+ <span class="has-tooltip-top" data-tooltip={dateTooltip}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ onClick={() => {
+ setPickDate(true);
+ }}
+ />
+ </span>
+ </div>
+ <div class="control">
+ <span class="has-tooltip-left" data-tooltip={dateTooltip}>
+ <a
+ class="button is-fullwidth"
+ onClick={() => {
+ setPickDate(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <DatePicker
+ opened={pickDate}
+ closeFunction={() => setPickDate(false)}
+ dateReceiver={onSelectDate}
+ />
+
+ <CardTable
+ orders={orders}
+ onCreate={onCreate}
+ onCopyURL={onCopyURL}
+ onSelect={onSelectOrder}
+ onRefund={onRefundOrder}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
new file mode 100644
index 000000000..b2806bb79
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -0,0 +1,417 @@
+/*
+ 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)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputGroup } from "../../../../components/form/InputGroup.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { mergeRefunds } from "../../../../utils/amount.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
+interface Props {
+ orders: Entity[];
+ onRefund: (value: Entity) => void;
+ onCopyURL: (id: string) => void;
+ onCreate: () => void;
+ onSelect: (order: Entity) => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ orders,
+ onCreate,
+ onRefund,
+ onCopyURL,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Orders</i18n.Translate>
+ </p>
+
+ <div class="card-header-icon" aria-label="more options" />
+
+ <div class="card-header-icon" aria-label="more options">
+ <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}>
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {orders.length > 0 ? (
+ <Table
+ instances={orders}
+ onSelect={onSelect}
+ onRefund={onRefund}
+ onCopyURL={(o) => onCopyURL(o.id)}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onRefund: (id: Entity) => void;
+ onCopyURL: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function Table({
+ instances,
+ onSelect,
+ onRefund,
+ onCopyURL,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer orders</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Date</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 100 }}>
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 400 }}>
+ <i18n.Translate>Summary</i18n.Translate>
+ </th>
+ <th style={{ minWidth: 50 }} />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.summary}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ {i.refundable && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onRefund(i)}
+ >
+ <i18n.Translate>Refund</i18n.Translate>
+ </button>
+ )}
+ {!i.paid && (
+ <button
+ class="button is-small is-info jb-modal"
+ type="button"
+ onClick={(): void => onCopyURL(i)}
+ >
+ <i18n.Translate>copy url</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older orders</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No orders have been found matching your query!
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface RefundModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
+ order: MerchantBackend.Orders.MerchantOrderStatusResponse;
+}
+
+export function RefundModal({
+ order,
+ onCancel,
+ onConfirm,
+}: RefundModalProps): VNode {
+ type State = { mainReason?: string; description?: string; refund?: string };
+ const [form, setValue] = useState<State>({});
+ const [settings] = useSettings();
+ const { i18n } = useTranslationContext();
+ // const [errors, setErrors] = useState<FormErrors<State>>({});
+
+ const refunds = (
+ order.order_status === "paid" ? order.refund_details : []
+ ).reduce(mergeRefunds, []);
+
+ const config = useConfigContext();
+ const totalRefunded = refunds
+ .map((r) => r.amount)
+ .reduce(
+ (p, c) => Amounts.add(p, Amounts.parseOrThrow(c)).amount,
+ Amounts.zeroOfCurrency(config.currency),
+ );
+ const orderPrice =
+ order.order_status === "paid"
+ ? Amounts.parseOrThrow(order.contract_terms.amount)
+ : undefined;
+ const totalRefundable = !orderPrice
+ ? Amounts.zeroOfCurrency(totalRefunded.currency)
+ : refunds.length
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
+
+ const isRefundable = Amounts.isNonZero(totalRefundable);
+ const duplicatedText = i18n.str`duplicated`;
+
+ const errors: FormErrors<State> = {
+ mainReason: !form.mainReason ? i18n.str`required` : undefined,
+ description:
+ !form.description && form.mainReason !== duplicatedText
+ ? i18n.str`required`
+ : undefined,
+ refund: !form.refund
+ ? i18n.str`required`
+ : !Amounts.parse(form.refund)
+ ? i18n.str`invalid format`
+ : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
+ ? i18n.str`this value exceed the refundable amount`
+ : undefined,
+ };
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ try {
+ if (!form.refund) return;
+ onConfirm({
+ refund: Amounts.stringify(
+ Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount,
+ ),
+ reason:
+ form.description === undefined
+ ? form.mainReason || ""
+ : `${form.mainReason}: ${form.description}`,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ //FIXME: parameters in the translation
+ return (
+ <ConfirmModal
+ description="refund"
+ danger
+ active
+ disabled={!isRefundable || hasErrors}
+ onCancel={onCancel}
+ onConfirm={validateAndConfirm}
+ >
+ {refunds.length > 0 && (
+ <div class="columns">
+ <div class="column is-12">
+ <InputGroup
+ name="asd"
+ label={`${Amounts.stringify(totalRefunded)} was already refunded`}
+ >
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>date</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>amount</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>reason</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map((r) => {
+ return (
+ <tr key={r.timestamp.t_s}>
+ <td>
+ {r.timestamp.t_s === "never"
+ ? "never"
+ : format(
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td>{r.amount}</td>
+ <td>{r.reason}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </InputGroup>
+ </div>
+ </div>
+ )}
+
+ {isRefundable && (
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={(d) => setValue(d as any)}
+ >
+ <InputCurrency<State>
+ name="refund"
+ label={i18n.str`Refund`}
+ tooltip={i18n.str`amount to be refunded`}
+ >
+ <i18n.Translate>Max refundable:</i18n.Translate>{" "}
+ {Amounts.stringify(totalRefundable)}
+ </InputCurrency>
+ <InputSelector
+ name="mainReason"
+ label={i18n.str`Reason`}
+ values={[
+ i18n.str`Choose one...`,
+ duplicatedText,
+ i18n.str`requested by the customer`,
+ i18n.str`other`,
+ ]}
+ tooltip={i18n.str`why this order is being refunded`}
+ />
+ {form.mainReason && form.mainReason !== duplicatedText ? (
+ <Input<State>
+ label={i18n.str`Description`}
+ name="description"
+ tooltip={i18n.str`more information to give context`}
+ />
+ ) : undefined}
+ </FormProvider>
+ )}
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
new file mode 100644
index 000000000..34c7d348a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -0,0 +1,231 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ InstanceOrderFilter,
+ useInstanceOrders,
+ useOrderAPI,
+ useOrderDetails,
+} from "../../../../hooks/order.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { RefundModal } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+}
+
+export default function OrderList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
+ const [orderToBeRefunded, setOrderToBeRefunded] = useState<
+ MerchantBackend.Orders.OrderHistoryEntry | undefined
+ >(undefined);
+
+ const setNewDate = (date?: Date): void =>
+ setFilter((prev) => ({ ...prev, date }));
+
+ const result = useInstanceOrders(filter, setNewDate);
+ const { refundOrder, getPaymentURL } = useOrderAPI();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
+ const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
+ const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
+ const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
+ const isWiredActive = filter.wired === "yes" ? "is-active" : "";
+ const isAllActive =
+ filter.paid === undefined &&
+ filter.refunded === undefined &&
+ filter.wired === undefined
+ ? "is-active"
+ : "";
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getPaymentURL}
+ onSelect={onSelect}
+ description={i18n.str`jump to order with the given product ID`}
+ palceholder={i18n.str`order id`}
+ />
+
+ <ListPage
+ orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
+ onLoadMoreBefore={result.loadMorePrev}
+ hasMoreBefore={!result.isReachingStart}
+ onLoadMoreAfter={result.loadMore}
+ hasMoreAfter={!result.isReachingEnd}
+ onSelectOrder={(order) => onSelect(order.id)}
+ onRefundOrder={(value) => setOrderToBeRefunded(value)}
+ isAllActive={isAllActive}
+ isNotWiredActive={isNotWiredActive}
+ isWiredActive={isWiredActive}
+ isPaidActive={isPaidActive}
+ isNotPaidActive={isNotPaidActive}
+ isRefundedActive={isRefundedActive}
+ jumpToDate={filter.date}
+ onCopyURL={(id) =>
+ getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
+ }
+ onCreate={onCreate}
+ onSelectDate={setNewDate}
+ onShowAll={() => setFilter({})}
+ onShowNotPaid={() => setFilter({ paid: "no" })}
+ onShowPaid={() => setFilter({ paid: "yes" })}
+ onShowRefunded={() => setFilter({ refunded: "yes" })}
+ onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
+ onShowWired={() => setFilter({ wired: "yes" })}
+ />
+
+ {orderToBeRefunded && (
+ <RefundModalForTable
+ id={orderToBeRefunded.order_id}
+ onCancel={() => setOrderToBeRefunded(undefined)}
+ onConfirm={(value) =>
+ refundOrder(orderToBeRefunded.order_id, value)
+ .then(() =>
+ setNotif({
+ message: i18n.str`refund created successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ .then(() => setOrderToBeRefunded(undefined))
+ }
+ onLoadError={(error) => {
+ setNotif({
+ message: i18n.str`could not create the refund`,
+ type: "ERROR",
+ description: error.message,
+ });
+ setOrderToBeRefunded(undefined);
+ return <div />;
+ }}
+ onUnauthorized={onUnauthorized}
+ onNotFound={() => {
+ setNotif({
+ message: i18n.str`could not get the order to refund`,
+ type: "ERROR",
+ // description: error.message
+ });
+ setOrderToBeRefunded(undefined);
+ return <div />;
+ }}
+ />
+ )}
+ </section>
+ );
+}
+
+interface RefundProps {
+ id: string;
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCancel: () => void;
+ onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void;
+}
+
+function RefundModalForTable({
+ id,
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onConfirm,
+ onCancel,
+}: RefundProps): VNode {
+ const result = useOrderDetails(id);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <RefundModal
+ order={result.data}
+ onCancel={onCancel}
+ onConfirm={onConfirm}
+ />
+ );
+}
+
+async function copyToClipboard(text: string): Promise<void> {
+ return navigator.clipboard.writeText(text);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
new file mode 100644
index 000000000..26f851cc8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
new file mode 100644
index 000000000..5f1ae26a3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -0,0 +1,180 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { isBase32RFC3548Charset, randomBase32Key } from "../../../../utils/crypto.js";
+import { QR } from "../../../../components/exception/QR.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const backend = useBackendContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {
+ otp_device_id: !state.otp_device_id ? i18n.str`required`
+ : !/[a-zA-Z0-9]*/.test(state.otp_device_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined,
+ otp_key: !state.otp_key ? i18n.str`required` :
+ !isBase32RFC3548Charset(state.otp_key)
+ ? i18n.str`just letters and numbers from 2 to 7`
+ : state.otp_key.length !== 32
+ ? i18n.str`size of the key should be 32`
+ : undefined,
+ otp_device_description: !state.otp_device_description ? i18n.str`required`
+ : !/[a-zA-Z0-9]*/.test(state.otp_device_description)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Internal id on the system`}
+ />
+ <Input<Entity>
+ name="otp_device_description"
+ label={i18n.str`Descripiton`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ expand
+ name="otp_key"
+ label={i18n.str`Device key`}
+ inputType={showKey ? "text" : "password"}
+ help="Be sure to be very hard to guess or use the random generator"
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfterAction={() => {
+ setShowKey(!showKey);
+ }}
+ addonAfter={
+ <span class="icon" >
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: randomBase32Key() }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ }
+ />
+ </Fragment>
+ ) : undefined}
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..db3842711
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -0,0 +1,104 @@
+/*
+ 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/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useBackendContext } from "../../../../context/backend.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const issuer = new URL(backendURL).hostname;
+ const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
+ const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
+
+ return (
+ <Template onConfirm={onConfirm} >
+ <p class="is-size-5">
+ <i18n.Translate>
+ You can scan the next QR code with your device or safe the key before continue.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.otp_device_id}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label"><i18n.Translate>Description</i18n.Translate></label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.otp_device_description}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <QR
+ text={qrText}
+ />
+ <div
+ style={{
+ color: "grey",
+ fontSize: "small",
+ width: 200,
+ textAlign: "center",
+ margin: "auto",
+ wordBreak: "break-all",
+ }}
+ >
+ {qrTextSafe}
+ </div>
+ </Template>
+ );
+}
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
new file mode 100644
index 000000000..648846793
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
@@ -0,0 +1,70 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createOtpDevice } = useOtpDeviceAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null)
+
+ if (created) {
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createOtpDevice(request)
+ .then((d) => {
+ setCreated(request)
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
new file mode 100644
index 000000000..b18049674
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/OtpDevices/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
new file mode 100644
index 000000000..4efee9781
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ devices: MerchantBackend.OTP.OtpDeviceEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+ onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ devices={devices.map((o) => ({
+ ...o,
+ id: String(o.otp_device_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
new file mode 100644
index 000000000..0c28027fe
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -0,0 +1,211 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceEntry;
+
+interface Props {
+ devices: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new devices`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {devices.length > 0 ? (
+ <Table
+ instances={devices}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer devices</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.otp_device_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected devices from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older devices</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no devices yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
new file mode 100644
index 000000000..2aae8738a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -0,0 +1,106 @@
+/*
+ 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)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListOtpDevices({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteOtpDevice } = useOtpDeviceAPI();
+ const result = useInstanceOtpDevices({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.otp_devices}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) =>
+ deleteOtpDevice(e.otp_device_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`validator delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the validator`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
new file mode 100644
index 000000000..d6b1d65e0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/OtpDevices/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
new file mode 100644
index 000000000..b82807cc7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
@@ -0,0 +1,171 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { randomBase32Key } from "../../../../utils/crypto.js";
+
+type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ device: Entity;
+}
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(device);
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ 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">
+ Device: <b>{device.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_description"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ name="otp_key"
+ label={i18n.str`Device key`}
+ readonly={state.otp_key === undefined}
+ inputType={showKey ? "text" : "password"}
+ help={state.otp_key === undefined ? "Not modified" : "Be sure to be very hard to guess or use the random generator"}
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfterAction={() => {
+ setShowKey(!showKey);
+ }}
+ addonAfter={
+ <span class="icon" onClick={() => {
+ setShowKey(!showKey);
+ }}>
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ state.otp_key === undefined ? <button
+
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: "" }));
+ }}
+ class="button">change key</button> :
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: randomBase32Key() }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ }
+ />
+ </Fragment>
+ ) : undefined} </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
new file mode 100644
index 000000000..52f6c6c29
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -0,0 +1,102 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js";
+
+export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ vid: string;
+}
+export default function UpdateValidator({
+ vid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateOtpDevice } = useOtpDeviceAPI();
+ const result = useOtpDeviceDetails(vid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ device={{
+ id: vid,
+ otp_algorithm: result.data.otp_algorithm,
+ otp_device_description: result.data.device_description,
+ otp_key: undefined,
+ otp_ctr: result.data.otp_ctr
+ }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateOtpDevice(vid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
new file mode 100644
index 000000000..2fc0819bb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Product/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
new file mode 100644
index 000000000..becaf8f3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
@@ -0,0 +1,80 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductAddDetail & {
+ product_id: string;
+};
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onCreate(result);
+ return Promise.reject();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm onSubscribe={addFormSubmitter} />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..6b02430cc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -0,0 +1,72 @@
+/*
+ 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/>
+ */
+import { h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { Entity } from "./index.js";
+import emptyImage from "../../assets/empty.png";
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Image</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <img src={entity.image} style={{ width: 200, height: 200 }} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Description</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <textarea class="input" readonly value={entity.description} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Price</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input class="input" readonly value={entity.price} />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Template>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
new file mode 100644
index 000000000..775690bd1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -0,0 +1,60 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
+ const { createProduct } = useProductAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
+ return createProduct(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
new file mode 100644
index 000000000..c2c4d548c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
@@ -0,0 +1,61 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Product/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onSelect: { action: "onSelect" },
+ onDelete: { action: "onDelete" },
+ onUpdate: { action: "onUpdate" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+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: {},
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
new file mode 100644
index 000000000..275f855cb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -0,0 +1,496 @@
+/*
+ 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)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import emptyImage from "../../../../assets/empty.png";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & WithId;
+
+interface Props {
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ onSelect: (product: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCreate: () => void;
+ selected?: boolean;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-shopping" />
+ </span>
+ <i18n.Translate>Inventory</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add product to inventory`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {instances.length > 0 ? (
+ <Table
+ instances={instances}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ onUpdate={onUpdate}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string | undefined;
+ instances: Entity[];
+ onSelect: (id: Entity) => void;
+ onUpdate: (
+ id: string,
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string | undefined>;
+}
+
+function Table({
+ rowSelection,
+ rowSelectionHandler,
+ instances,
+ onSelect,
+ onUpdate,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Image</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Price per unit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Taxes</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sales</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Stock</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sold</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ const restStockInfo = !i.next_restock
+ ? ""
+ : i.next_restock.t_s === "never"
+ ? "never"
+ : `restock at ${format(
+ new Date(i.next_restock.t_s * 1000),
+ dateFormatForSettings(settings),
+ )}`;
+ let stockInfo: ComponentChildren = "";
+ if (i.total_stock < 0) {
+ stockInfo = "infinite";
+ } else {
+ const totalStock = i.total_stock - i.total_lost - i.total_sold;
+ stockInfo = (
+ <label title={restStockInfo}>
+ {totalStock} {i.unit}
+ </label>
+ );
+ }
+
+ const isFree = Amounts.isZero(Amounts.parseOrThrow(i.price));
+
+ return (
+ <Fragment key={i.id}>
+ <tr key="info">
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <img
+ src={i.image ? i.image : emptyImage}
+ style={{
+ border: "solid black 1px",
+ maxHeight: "2em",
+ width: "auto",
+ height: "auto",
+ }}
+ />
+ </td>
+ <td
+ class="has-tooltip-right"
+ data-tooltip={i.description}
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {sum(i.taxes)}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {difference(i.price, sum(i.taxes))}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {stockInfo}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.id && rowSelectionHandler(i.id)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ <span style={{"whiteSpace":"nowrap"}}>
+
+ {i.total_sold} {i.unit}
+ </span>
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`go to product update page`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSelect(i)}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </span>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`remove this product from the database`}
+ >
+ <button
+ class="button is-small is-danger"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </td>
+ </tr>
+ {rowSelection === i.id && (
+ <tr key="form">
+ <td colSpan={10}>
+ <FastProductUpdateForm
+ product={i}
+ onUpdate={(prod) =>
+ onUpdate(i.id, prod).then((r) =>
+ rowSelectionHandler(undefined),
+ )
+ }
+ onCancel={() => rowSelectionHandler(undefined)}
+ />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+interface FastProductUpdateFormProps {
+ product: Entity;
+ onUpdate: (
+ data: MerchantBackend.Products.ProductPatchDetail,
+ ) => Promise<void>;
+ onCancel: () => void;
+}
+interface FastProductUpdate {
+ incoming: number;
+ lost: number;
+ price: string;
+}
+interface UpdatePrice {
+ price: string;
+}
+
+function FastProductWithInfiniteStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<UpdatePrice>({ price: product.price });
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`update the product with new price`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-expanded">
+
+ <div class="buttons mt-5">
+
+ <button class="button mt-5" onClick={onCancel}>
+ <i18n.Translate>Clone</i18n.Translate>
+ </button>
+ </div>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`update product with new price`}
+ >
+ <button
+ class="button is-info"
+ onClick={() =>
+ onUpdate({
+ ...product,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductWithManagedStockUpdateForm({
+ product,
+ onUpdate,
+ onCancel,
+}: FastProductUpdateFormProps) {
+ const [value, valueHandler] = useState<FastProductUpdate>({
+ incoming: 0,
+ lost: 0,
+ price: product.price,
+ });
+
+ const currentStock =
+ product.total_stock - product.total_sold - product.total_lost;
+
+ const errors: FormErrors<FastProductUpdate> = {
+ lost:
+ currentStock + value.incoming < value.lost
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <FormProvider<FastProductUpdate>
+ name="added"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler as any}
+ >
+ <InputNumber<FastProductUpdate>
+ name="incoming"
+ label={i18n.str`Incoming`}
+ tooltip={i18n.str`add more elements to the inventory`}
+ />
+ <InputNumber<FastProductUpdate>
+ name="lost"
+ label={i18n.str`Lost`}
+ tooltip={i18n.str`report elements lost in the inventory`}
+ />
+ <InputCurrency<FastProductUpdate>
+ name="price"
+ label={i18n.str`Price`}
+ tooltip={i18n.str`new price for the product`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={
+ hasErrors
+ ? i18n.str`the are value with errors`
+ : i18n.str`update product with new stock and price`
+ }
+ >
+ <button
+ class="button is-info"
+ disabled={hasErrors}
+ onClick={() =>
+ onUpdate({
+ ...product,
+ total_stock: product.total_stock + value.incoming,
+ total_lost: product.total_lost + value.lost,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </span>
+ </div>
+ </Fragment>
+ );
+}
+
+function FastProductUpdateForm(props: FastProductUpdateFormProps) {
+ return props.product.total_stock === -1 ? (
+ <FastProductWithInfiniteStockUpdateForm {...props} />
+ ) : (
+ <FastProductWithManagedStockUpdateForm {...props} />
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no products yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function difference(price: string, tax: number) {
+ if (!tax) return price;
+ const ps = price.split(":");
+ const p = parseInt(ps[1], 10);
+ ps[1] = `${p - tax}`;
+ return ps.join(":");
+}
+function sum(taxes: MerchantBackend.Tax[]) {
+ return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0);
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
new file mode 100644
index 000000000..942b5d0ac
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -0,0 +1,150 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useInstanceProducts,
+ useProductAPI,
+} from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+export default function ProductList({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const result = useInstanceProducts();
+ const { deleteProduct, updateProduct, getProduct } = useProductAPI();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={getProduct}
+ onSelect={onSelect}
+ description={i18n.str`jump to product with the given product ID`}
+ palceholder={i18n.str`product id`}
+ />
+
+ <CardTable
+ instances={result.data}
+ onCreate={onCreate}
+ onUpdate={(id, prod) =>
+ updateProduct(id, prod)
+ .then(() =>
+ setNotif({
+ message: i18n.str`product updated successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not update the product`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ onSelect={(product) => onSelect(product.id)}
+ onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) =>
+ setDeleting(prod)
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete product`}
+ description={`Delete the product "${deleting.description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteProduct(deleting.id);
+ setNotif({
+ message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete product`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the product named <b>&quot;{deleting.description}&quot;</b> (ID:{" "}
+ <b>{deleting.id}</b>), the stock and related information will be lost
+ </p>
+ <p class="warning">
+ Deleting an product <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
new file mode 100644
index 000000000..a85b13b8b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
@@ -0,0 +1,73 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Product/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const WithManagedStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ 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 WithInfiniteStock = createExample(TestedComponent, {
+ product: {
+ product_id: "20102-ASDAS-QWE",
+ description: "description1",
+ description_i18n: {} as any,
+ image: "",
+ price: "TESTKUDOS:10",
+ taxes: [],
+ total_lost: 10,
+ total_sold: 5,
+ total_stock: -1,
+ unit: "bar",
+ address: {},
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
new file mode 100644
index 000000000..97715171e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
@@ -0,0 +1,99 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import { ProductForm } from "../../../../components/product/ProductForm.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useListener } from "../../../../hooks/listener.js";
+
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ product: Entity;
+}
+
+export function UpdatePage({ product, onUpdate, onBack }: Props): VNode {
+ const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
+ (result) => {
+ if (result) return onUpdate(result);
+ return Promise.resolve();
+ },
+ );
+
+ const { i18n } = useTranslationContext();
+
+ 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>Product id:</i18n.Translate>
+ <b>{product.product_id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <ProductForm
+ initial={product}
+ onSubscribe={addFormSubmitter}
+ alreadyExist
+ />
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ !submitForm
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={!submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
new file mode 100644
index 000000000..8e0f7647f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -0,0 +1,95 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Products.ProductAddDetail;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ pid: string;
+}
+export default function UpdateProduct({
+ pid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateProduct } = useProductAPI();
+ const result = useProductDetails(pid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ product={{ ...result.data, product_id: pid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateProduct(pid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create product`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
new file mode 100644
index 000000000..5542c028a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
@@ -0,0 +1,43 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Reserve/Create",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
new file mode 100644
index 000000000..e46941b6d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -0,0 +1,277 @@
+/*
+ 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)
+ */
+
+import { HttpError, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ PAYTO_WIRE_METHOD_LOOKUP,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+import { useBackendBaseRequest } from "../../../../hooks/backend.js";
+import { parsePaytoUri } from "@gnu-taler/taler-util";
+
+type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+enum Steps {
+ EXCHANGE,
+ WIRE_METHOD,
+}
+
+interface ViewProps {
+ step: Steps;
+ setCurrentStep: (s: Steps) => void;
+ reserve: Partial<Entity>;
+ onBack?: () => void;
+ submitForm: () => Promise<void>;
+ setReserve: StateUpdater<Partial<Entity>>;
+}
+function ViewStep({
+ step,
+ setCurrentStep,
+ reserve,
+ onBack,
+ submitForm,
+ setReserve,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
+ const {request} = useApiContext()
+ const [wireMethods, setWireMethods] = useState<Array<string>>([]);
+ const [exchangeQueryError, setExchangeQueryError] = useState<
+ string | undefined
+ >(undefined);
+
+ useEffect(() => {
+ setExchangeQueryError(undefined);
+ }, [reserve.exchange_url]);
+
+ switch (step) {
+ case Steps.EXCHANGE: {
+ const errors: FormErrors<Entity> = {
+ initial_balance: !reserve.initial_balance
+ ? "cannot be empty"
+ : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0)
+ ? i18n.str`it should be greater than 0`
+ : undefined,
+ exchange_url: !reserve.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(reserve.exchange_url)
+ ? i18n.str`must be a valid URL`
+ : !exchangeQueryError
+ ? undefined
+ : exchangeQueryError,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ class="has-tooltip-left"
+ onClick={() => {
+ if (!reserve.exchange_url) {
+ return Promise.resolve();
+ }
+
+ return request<any>(reserve.exchange_url, "keys")
+ .then((r) => {
+ console.log(r)
+ if (r.loading) return;
+ if (r.ok) {
+ const wireMethods = r.data.accounts.map((a: any) => {
+ const p = parsePaytoUri(a.payto_uri);
+ const r = p?.targetType
+ return r
+ }).filter((x:any) => !!x);
+ setWireMethods(Array.from(new Set(wireMethods)));
+ }
+ setCurrentStep(Steps.WIRE_METHOD);
+ return;
+ })
+ .catch((r: RequestError<{}>) => {
+ console.log(r.cause)
+ setExchangeQueryError(r.cause.message);
+ });
+ }}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+
+ case Steps.WIRE_METHOD: {
+ const errors: FormErrors<Entity> = {
+ wire_method: !reserve.wire_method
+ ? i18n.str`cannot be empty`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+ return (
+ <Fragment>
+ <FormProvider<Entity>
+ object={reserve}
+ errors={errors}
+ valueHandler={setReserve}
+ >
+ <InputCurrency<Entity>
+ name="initial_balance"
+ label={i18n.str`Initial balance`}
+ tooltip={i18n.str`balance prior to deposit`}
+ readonly
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`URL of exchange`}
+ readonly
+ />
+ <InputSelector<Entity>
+ name="wire_method"
+ label={i18n.str`Wire method`}
+ tooltip={i18n.str`method to use for wire transfer`}
+ values={wireMethods}
+ placeholder={i18n.str`Select one wire method`}
+ />
+ </FormProvider>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button
+ class="button"
+ onClick={() => setCurrentStep(Steps.EXCHANGE)}
+ >
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ onClick={submitForm}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </Fragment>
+ );
+ }
+ }
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const [reserve, setReserve] = useState<Partial<Entity>>({});
+
+ const submitForm = () => {
+ return onCreate(reserve as Entity);
+ };
+
+ const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE);
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}>
+ <a style={{ cursor: "initial" }}>
+ <span>Step 1: Specify exchange</span>
+ </a>
+ </li>
+ <li
+ class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""}
+ >
+ <a style={{ cursor: "initial" }}>
+ <span>Step 2: Select wire method</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <ViewStep
+ step={currentStep}
+ reserve={reserve}
+ setCurrentStep={setCurrentStep}
+ setReserve={setReserve}
+ submitForm={submitForm}
+ onBack={onBack}
+ />
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
new file mode 100644
index 000000000..445ca3ef0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
@@ -0,0 +1,120 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+export default {
+ title: "Pages/Reserve/CreatedSuccessfully",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+export const OneBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const ThreeBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
+
+export const NoBankAccount = tests.createExample(TestedComponent, {
+ entity: {
+ request: {
+ exchange_url: "http://exchange.taler/",
+ initial_balance: "TESTKUDOS:1",
+ wire_method: "x-taler-bank",
+ },
+ response: {
+ accounts: [
+ {
+ payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ {
+ payto_uri: "payto://x-taler-bank",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "asd",
+ conversion_url: "",
+ },
+ ],
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..1d512c843
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -0,0 +1,190 @@
+/*
+ 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/>
+ */
+
+import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { QR } from "../../../../components/exception/QR.js";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { MerchantBackend, WireAccount } from "../../../../declaration.js";
+
+type Entity = {
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+};
+
+interface Props {
+ entity: Entity;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+function isNotUndefined<X>(x: X | undefined): x is X {
+ return !!x;
+}
+
+export function CreatedSuccessfully({
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.request.initial_balance}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Wire transfer subject</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.response.reserve_pub}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <ShowAccountsOfReserveAsQRWithLink
+ accounts={entity.response.accounts ?? []}
+ message={entity.response.reserve_pub}
+ amount={entity.request.initial_balance}
+ />
+ </Template>
+ );
+}
+
+export function ShowAccountsOfReserveAsQRWithLink({
+ accounts,
+ message,
+ amount,
+}: {
+ accounts: WireAccount[];
+ message: string;
+ amount: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const accountsInfo = !accounts
+ ? []
+ : accounts
+ .map((acc) => {
+ const p = parsePaytoUri(acc.payto_uri);
+ if (p) {
+ p.params["message"] = message;
+ p.params["amount"] = amount;
+ }
+ return p;
+ })
+ .filter(isNotUndefined);
+
+ const links = accountsInfo.map((a) => stringifyPaytoUri(a));
+
+ if (links.length === 0) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ The reserve have invalid accounts. List of invalid payto URIs below:
+ </p>
+ <ul>
+ {accounts.map((a, idx) => {
+ return <li key={idx}>{a.payto_uri}</li>;
+ })}
+ </ul>
+ </Fragment>
+ );
+ }
+
+ if (links.length === 1) {
+ return (
+ <Fragment>
+ <p class="is-size-5">
+ <i18n.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.
+ </i18n.Translate>
+ </p>
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank account</b>
+ </p>
+ <QR text={links[0]} />
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by opening this
+ URI:
+ </i18n.Translate>
+ </p>
+ <pre>
+ <a target="_blank" rel="noreferrer" href={links[0]}>
+ {links[0]}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ }
+
+ return (
+ <div>
+ <p class="is-size-5">
+ <i18n.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 one of the indicated account of the exchange.
+ </i18n.Translate>
+ </p>
+
+ <p style={{ margin: 10 }}>
+ <b>Exchange bank accounts</b>
+ </p>
+ <p class="is-size-5">
+ <i18n.Translate>
+ If your system supports RFC 8905, you can do this by clicking on the
+ URI below the QR code:
+ </i18n.Translate>
+ </p>
+ {links.map((link) => {
+ return (
+ <Fragment>
+ <QR text={link} />
+ <pre>
+ <a target="_blank" rel="noreferrer" href={link}>
+ {link}
+ </a>
+ </pre>
+ </Fragment>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
new file mode 100644
index 000000000..4bbaf1459
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
@@ -0,0 +1,71 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useReservesAPI } from "../../../../hooks/reserves.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+import { CreatePage } from "./CreatePage.js";
+interface Props {
+ onBack: () => void;
+ onConfirm: () => void;
+}
+export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
+ const { createReserve } = useReservesAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ const [createdOk, setCreatedOk] = useState<
+ | {
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+ }
+ | undefined
+ >(undefined);
+
+ if (createdOk) {
+ return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />;
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => {
+ return createReserve(request)
+ .then((r) => setCreatedOk({ request, response: r.data }))
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create reserve`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
new file mode 100644
index 000000000..d8840eeac
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
@@ -0,0 +1,266 @@
+/*
+ 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)
+ */
+
+import {
+ Amounts,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { QR } from "../../../../components/exception/QR.js";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDate } from "../../../../components/form/InputDate.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { SimpleModal } from "../../../../components/modal/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useRewardDetails } from "../../../../hooks/reserves.js";
+import { RewardInfo } from "./RewardInfo.js";
+import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.ReserveDetail;
+type CT = MerchantBackend.ContractTerms;
+
+interface Props {
+ onBack: () => void;
+ selected: Entity;
+ id: string;
+}
+
+export function DetailPage({ id, selected, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const didExchangeAckTransfer = Amounts.isNonZero(
+ Amounts.parseOrThrow(selected.exchange_initial_amount),
+ );
+
+ return (
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div class="section main-section">
+ <FormProvider object={{ ...selected, id }} valueHandler={null}>
+ <InputDate<Entity>
+ name="creation_time"
+ label={i18n.str`Created at`}
+ readonly
+ />
+ <InputDate<Entity>
+ name="expiration_time"
+ label={i18n.str`Valid until`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="merchant_initial_amount"
+ label={i18n.str`Created balance`}
+ readonly
+ />
+ <TextField<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ readonly
+ >
+ <a target="_blank" rel="noreferrer" href={selected.exchange_url}>
+ {selected.exchange_url}
+ </a>
+ </TextField>
+
+ {didExchangeAckTransfer && (
+ <Fragment>
+ <InputCurrency<Entity>
+ name="exchange_initial_amount"
+ label={i18n.str`Exchange balance`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="pickup_amount"
+ label={i18n.str`Picked up`}
+ readonly
+ />
+ <InputCurrency<Entity>
+ name="committed_amount"
+ label={i18n.str`Committed`}
+ readonly
+ />
+ </Fragment>
+ )}
+ <Input name="id" label={i18n.str`Subject`} readonly />
+ </FormProvider>
+
+ {didExchangeAckTransfer ? (
+ <Fragment>
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash-register" />
+ </span>
+ <i18n.Translate>Rewards</i18n.Translate>
+ </p>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {selected.rewards && selected.rewards.length > 0 ? (
+ <Table rewards={selected.rewards} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ ) : selected.accounts ? (
+ <ShowAccountsOfReserveAsQRWithLink
+ accounts={selected.accounts}
+ amount={selected.merchant_initial_amount}
+ message={id}
+ />
+ ) : undefined}
+
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Back</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ No reward has been authorized from this reserve
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+interface TableProps {
+ rewards: MerchantBackend.Rewards.RewardStatusEntry[];
+}
+
+function Table({ rewards }: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Authorized</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Picked up</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Reason</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expiration</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {rewards.map((t, i) => {
+ return <RewardRow id={t.reward_id} key={i} entry={t} />;
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function RewardRow({
+ id,
+ entry,
+}: {
+ id: string;
+ entry: MerchantBackend.Rewards.RewardStatusEntry;
+}) {
+ const [selected, setSelected] = useState(false);
+ const result = useRewardDetails(id);
+ const [settings] = useSettings();
+ if (result.loading) {
+ return (
+ <tr>
+ <td>...</td>
+ <td>...</td>
+ <td>...</td>
+ <td>...</td>
+ </tr>
+ );
+ }
+ if (!result.ok) {
+ return (
+ <tr>
+ <td>...</td> {/* authorized */}
+ <td>{entry.total_amount}</td>
+ <td>{entry.reason}</td>
+ <td>...</td> {/* expired */}
+ </tr>
+ );
+ }
+ const info = result.data;
+ function onSelect() {
+ setSelected(true);
+ }
+ return (
+ <Fragment>
+ {selected && (
+ <SimpleModal
+ description="reward"
+ active
+ onCancel={() => setSelected(false)}
+ >
+ <RewardInfo id={id} amount={info.total_authorized} entity={info} />
+ </SimpleModal>
+ )}
+ <tr>
+ <td onClick={onSelect}>{info.total_authorized}</td>
+ <td onClick={onSelect}>{info.total_picked_up}</td>
+ <td onClick={onSelect}>{info.reason}</td>
+ <td onClick={onSelect}>
+ {info.expiration.t_s === "never"
+ ? "never"
+ : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ </tr>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
new file mode 100644
index 000000000..41c715f20
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
@@ -0,0 +1,126 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Reserve/Detail",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Funded = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ },
+});
+
+export const NotYetFunded = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:0",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ },
+});
+
+export const FundedWithEmptyRewards = createExample(TestedComponent, {
+ id: "THISISTHERESERVEID",
+ selected: {
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ accounts: [
+ {
+ payto_uri: "payto://x-taler-bank/bank.taler:8080/account",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ master_sig: "",
+ },
+ ],
+ exchange_url: "http://exchange.taler/",
+ rewards: [
+ {
+ reason: "asdasd",
+ reward_id: "123",
+ total_amount: "TESTKUDOS:1",
+ },
+ ],
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
new file mode 100644
index 000000000..780068a91
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
@@ -0,0 +1,88 @@
+/*
+ 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/>
+ */
+import { stringifyRewardUri } from "@gnu-taler/taler-util";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.RewardDetails;
+
+interface Props {
+ id: string;
+ entity: Entity;
+ amount: string;
+}
+
+export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
+ const { url: backendURL } = useBackendContext()
+ const [settings] = useSettings();
+ const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId })
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={amount} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field" style={{ overflowWrap: "anywhere" }}>
+ <p class="control">
+ <a target="_blank" rel="noreferrer" href={rewardURL}>
+ {rewardURL}
+ </a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Valid until</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={
+ !entity.expiration || entity.expiration.t_s === "never"
+ ? "never"
+ : format(
+ entity.expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ }
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
new file mode 100644
index 000000000..8e2a74529
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
@@ -0,0 +1,68 @@
+/*
+ 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)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useReserveDetails } from "../../../../hooks/reserves.js";
+import { DetailPage } from "./DetailPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ rid: string;
+
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onDelete: () => void;
+ onBack: () => void;
+}
+export default function DetailReserve({
+ rid,
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onBack,
+ onDelete,
+}: Props): VNode {
+ const result = useReserveDetails(rid);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+ return (
+ <Fragment>
+ <DetailPage selected={result.data} onBack={onBack} id={rid} />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
new file mode 100644
index 000000000..e205ee621
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
@@ -0,0 +1,124 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import * as yup from "yup";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import {
+ ConfirmModal,
+ ContinueModal,
+} from "../../../../components/modal/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { AuthorizeRewardSchema } from "../../../../schemas/index.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+
+interface AuthorizeRewardModalProps {
+ onCancel: () => void;
+ onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void;
+ rewardAuthorized?: {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+ };
+}
+
+export function AuthorizeRewardModal({
+ onCancel,
+ onConfirm,
+ rewardAuthorized,
+}: AuthorizeRewardModalProps): VNode {
+ // const result = useOrderDetails(id)
+ type State = MerchantBackend.Rewards.RewardCreateRequest;
+ const [form, setValue] = useState<Partial<State>>({});
+ const { i18n } = useTranslationContext();
+
+ // const [errors, setErrors] = useState<FormErrors<State>>({})
+ let errors: FormErrors<State> = {};
+ try {
+ AuthorizeRewardSchema.validateSync(form, { abortEarly: false });
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as any[];
+ errors = yupErrors.reduce(
+ (prev, cur) =>
+ !cur.path ? prev : { ...prev, [cur.path]: cur.message },
+ {},
+ );
+ }
+ }
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const validateAndConfirm = () => {
+ onConfirm(form as State);
+ };
+ if (rewardAuthorized) {
+ return (
+ <ContinueModal description="reward" active onConfirm={onCancel}>
+ <CreatedSuccessfully
+ entity={rewardAuthorized.response}
+ request={rewardAuthorized.request}
+ onConfirm={onCancel}
+ />
+ </ContinueModal>
+ );
+ }
+
+ return (
+ <ConfirmModal
+ description="New reward"
+ active
+ onCancel={onCancel}
+ disabled={hasErrors}
+ onConfirm={validateAndConfirm}
+ >
+ <FormProvider<State>
+ errors={errors}
+ object={form}
+ valueHandler={setValue}
+ >
+ <InputCurrency<State>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`amount of reward`}
+ />
+ <Input<State>
+ name="justification"
+ label={i18n.str`Justification`}
+ inputType="multiline"
+ tooltip={i18n.str`reason for the reward`}
+ />
+ <Input<State>
+ name="next_url"
+ label={i18n.str`URL after reward`}
+ tooltip={i18n.str`URL to visit after reward payment`}
+ />
+ </FormProvider>
+ </ConfirmModal>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..b78236bc7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
@@ -0,0 +1,102 @@
+/*
+ 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/>
+ */
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.RewardCreateConfirmation;
+
+interface Props {
+ entity: Entity;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+ onConfirm: () => void;
+ onCreateAnother?: () => void;
+}
+
+export function CreatedSuccessfully({
+ request,
+ entity,
+ onConfirm,
+ onCreateAnother,
+}: Props): VNode {
+ const [settings] = useSettings();
+ return (
+ <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Amount</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={request.amount} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Justification</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={request.justification} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input readonly class="input" value={entity.reward_status_url} />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">Valid until</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={
+ !entity.reward_expiration ||
+ entity.reward_expiration.t_s === "never"
+ ? "never"
+ : format(
+ entity.reward_expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ }
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
new file mode 100644
index 000000000..b070bbde3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
@@ -0,0 +1,96 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CardTable as TestedComponent } from "./Table.js";
+
+export default {
+ title: "Pages/Reserve/List",
+ component: TestedComponent,
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const AllFunded = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "reseverId",
+ active: true,
+ committed_amount: "TESTKUDOS:10",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ {
+ id: "reseverId2",
+ active: true,
+ committed_amount: "TESTKUDOS:13",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:10",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ ],
+});
+
+export const Empty = createExample(TestedComponent, {
+ instances: [],
+});
+
+export const OneNotYetFunded = createExample(TestedComponent, {
+ instances: [
+ {
+ id: "reseverId",
+ active: true,
+ committed_amount: "TESTKUDOS:0",
+ creation_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ exchange_initial_amount: "TESTKUDOS:0",
+ expiration_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ merchant_initial_amount: "TESTKUDOS:10",
+ pickup_amount: "TESTKUDOS:10",
+ reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS",
+ },
+ ],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
new file mode 100644
index 000000000..795e7ec82
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
@@ -0,0 +1,320 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId;
+
+interface Props {
+ instances: Entity[];
+ onNewReward: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+ onDelete: (id: Entity) => void;
+ onCreate: () => void;
+}
+
+export function CardTable({
+ instances,
+ onCreate,
+ onSelect,
+ onNewReward,
+ onDelete,
+}: Props): VNode {
+ const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
+ const amount = current.exchange_initial_amount;
+ if (amount.endsWith(":0")) {
+ prev[0] = prev[0].concat(current);
+ } else {
+ prev[1] = prev[1].concat(current);
+ }
+ return prev;
+ }, new Array<Array<Entity>>([], []));
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ {withoutFunds.length > 0 && (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <i18n.Translate>Reserves not yet funded</i18n.Translate>
+ </p>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ <TableWithoutFund
+ instances={withoutFunds}
+ onNewReward={onNewReward}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-cash" />
+ </span>
+ <i18n.Translate>Reserves ready</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options" />
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new reserve`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {withFunds.length > 0 ? (
+ <Table
+ instances={withFunds}
+ onNewReward={onNewReward}
+ onSelect={onSelect}
+ onDelete={onDelete}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+interface TableProps {
+ instances: Entity[];
+ onNewReward: (id: Entity) => void;
+ onDelete: (id: Entity) => void;
+ onSelect: (id: Entity) => void;
+}
+
+function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Created at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expires at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Initial</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Picked up</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Committed</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.creation_time.t_s === "never"
+ ? "never"
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.expiration_time.t_s === "never"
+ ? "never"
+ : format(
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.exchange_initial_amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.pickup_amount}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.committed_amount}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-danger has-tooltip-left"
+ data-tooltip={i18n.str`delete selected reserve from the database`}
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ Delete
+ </button>
+ <button
+ class="button is-small is-info has-tooltip-left"
+ data-tooltip={i18n.str`authorize new reward from selected reserve`}
+ type="button"
+ onClick={(): void => onNewReward(i)}
+ >
+ New Reward
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no ready reserves yet, add more pressing the + sign or fund
+ them
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
+
+function TableWithoutFund({
+ instances,
+ onSelect,
+ onDelete,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Created at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expires at</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Expected Balance</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.creation_time.t_s === "never"
+ ? "never"
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.expiration_time.t_s === "never"
+ ? "never"
+ : format(
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.merchant_initial_amount}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-small is-danger jb-modal has-tooltip-left"
+ type="button"
+ data-tooltip={i18n.str`delete selected reserve from the database`}
+ onClick={(): void => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
new file mode 100644
index 000000000..b26ff0000
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
@@ -0,0 +1,171 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceReserves,
+ useReservesAPI,
+} from "../../../../hooks/reserves.js";
+import { Notification } from "../../../../utils/types.js";
+import { AuthorizeRewardModal } from "./AutorizeRewardModal.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onSelect: (id: string) => void;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+
+interface RewardConfirmation {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
+}
+
+export default function ListRewards({
+ onUnauthorized,
+ onLoadError,
+ onNotFound,
+ onSelect,
+ onCreate,
+}: Props): VNode {
+ const result = useInstanceReserves();
+ const { deleteReserve, authorizeRewardReserve } = useReservesAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [reserveForReward, setReserveForReward] = useState<string | undefined>(
+ undefined,
+ );
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null);
+ const [rewardAuthorized, setRewardAuthorized] = useState<
+ RewardConfirmation | undefined
+ >(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ {reserveForReward && (
+ <AuthorizeRewardModal
+ onCancel={() => {
+ setReserveForReward(undefined);
+ setRewardAuthorized(undefined);
+ }}
+ rewardAuthorized={rewardAuthorized}
+ onConfirm={async (request) => {
+ try {
+ const response = await authorizeRewardReserve(
+ reserveForReward,
+ request,
+ );
+ setRewardAuthorized({
+ request,
+ response: response.data,
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`could not create the reward`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ setReserveForReward(undefined);
+ }
+ }}
+ />
+ )}
+
+ <CardTable
+ instances={result.data.reserves
+ .filter((r) => r.active)
+ .map((o) => ({ ...o, id: o.reserve_pub }))}
+ onCreate={onCreate}
+ onDelete={(reserve) => {
+ setDeleting(reserve)
+ }}
+ onSelect={(reserve) => onSelect(reserve.id)}
+ onNewReward={(reserve) => setReserveForReward(reserve.id)}
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete reserve`}
+ description={`Delete the reserve`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteReserve(deleting.reserve_pub);
+ setNotif({
+ message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete reserve`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the reserve for <b>&quot;{deleting.merchant_initial_amount}&quot;</b> you won't be able to create more rewards. <br />
+ Reserve ID: <b>{deleting.reserve_pub}</b>
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
new file mode 100644
index 000000000..c9d17ea3b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
new file mode 100644
index 000000000..947f3572c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -0,0 +1,259 @@
+/*
+ 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)
+ */
+
+import {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps };
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const devices = useInstanceOtpDevices()
+
+ const [state, setState] = useState<Partial<Entity>>({
+ template_contract: {
+ minimum_age: 0,
+ pay_duration: {
+ d_us: 1000 * 1000 * 60 * 30, //30 min
+ },
+ },
+ type: Steps.NON_FIXED,
+ });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_id: !state.template_id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.template_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onCreate(state as any);
+ };
+
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="template_id"
+ help={`${backendURL}/templates/${state.template_id ?? ""}`}
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ <Input<Entity>
+ name="otp_id"
+ label={i18n.str`OTP device`}
+ readonly
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ <InputSearchOnList
+ label={i18n.str`Search device`}
+ onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))}
+ list={deviceList.map(e => ({
+ description: e.device_description,
+ id: e.otp_device_id
+ }))}
+ />
+
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
new file mode 100644
index 000000000..a29ee53b6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTemplateAPI } from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { createTemplate } = useTemplateAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => {
+ return createTemplate(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
new file mode 100644
index 000000000..bf6062c34
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
@@ -0,0 +1,68 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ templates: MerchantBackend.Template.TemplateEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onSelect: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void;
+ onQR: (e: MerchantBackend.Template.TemplateEntry) => void;
+}
+
+export function ListPage({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onNewOrder,
+ onQR,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <CardTable
+ templates={templates.map((o) => ({
+ ...o,
+ id: String(o.template_id),
+ }))}
+ onQR={onQR}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
new file mode 100644
index 000000000..9fdf4ead9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -0,0 +1,235 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.TemplateEntry;
+
+interface Props {
+ templates: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ templates,
+ onCreate,
+ onDelete,
+ onSelect,
+ onQR,
+ onNewOrder,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Templates</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new templates`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {templates.length > 0 ? (
+ <Table
+ instances={templates}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onNewOrder={onNewOrder}
+ onQR={onQR}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onNewOrder: (e: Entity) => void;
+ onQR: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onNewOrder,
+ onQR,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer templates</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.template_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.template_description}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected templates from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`use template to create new order`}
+ onClick={() => onNewOrder(i)}
+ >
+ Use template
+ </button>
+ <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`create qr code for the template`}
+ onClick={() => onQR(i)}
+ >
+ QR
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more templates after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older templates</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no templates yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
new file mode 100644
index 000000000..b9767442f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -0,0 +1,152 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceTemplates,
+ useTemplateAPI,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+ onNewOrder: (id: string) => void;
+ onQR: (id: string) => void;
+}
+
+export default function ListTemplates({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onQR,
+ onSelect,
+ onNewOrder,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteTemplate, testTemplateExist } = useTemplateAPI();
+ const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Template.TemplateEntry | null>(null);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <JumpToElementById
+ testIfExist={testTemplateExist}
+ onSelect={onSelect}
+ description={i18n.str`jump to template with the given template ID`}
+ palceholder={i18n.str`template id`}
+ />
+
+ <ListPage
+ templates={result.data.templates}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.template_id);
+ }}
+ onNewOrder={(e) => {
+ onNewOrder(e.template_id);
+ }}
+ onQR={(e) => {
+ onQR(e.template_id);
+ }}
+ onDelete={(e: MerchantBackend.Template.TemplateEntry) => {
+ setDeleting(e)
+ }
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete template`}
+ description={`Delete the template "${deleting.template_description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteTemplate(deleting.template_id);
+ setNotif({
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete template`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the template <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
+ <b>{deleting.template_id}</b>) you may loose information
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
new file mode 100644
index 000000000..eb853c8ff
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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)
+ */
+
+import { QrPage as TestedComponent } from "./QrPage.js";
+
+export default {
+ title: "Pages/Templates/QR",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
new file mode 100644
index 000000000..5140aae3a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -0,0 +1,172 @@
+/*
+ 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)
+ */
+
+import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { QR } from "../../../../components/exception/QR.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Template.UsingTemplateDetails;
+
+interface Props {
+ contract: MerchantBackend.Template.TemplateContractDetails;
+ id: string;
+ onBack?: () => void;
+}
+
+export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+ const { id: instanceId } = useInstanceContext();
+ const config = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: contract.amount,
+ summary: contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {};
+
+ const fixedAmount = !!contract.amount;
+ const fixedSummary = !!contract.summary;
+
+ const templateParams: Record<string, string> = {}
+ if (!fixedAmount) {
+ if (state.amount) {
+ templateParams.amount = state.amount
+ } else {
+ templateParams.amount = config.currency
+ }
+ }
+
+ if (!fixedSummary) {
+ templateParams.summary = state.summary ?? ""
+ }
+
+ const merchantBaseUrl = new URL(backendURL).href;
+
+ const payTemplateUri = stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams
+ })
+
+ const issuer = encodeURIComponent(
+ `${new URL(backendURL).host}/${instanceId}`,
+ );
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <p class="is-size-5 mt-5 mb-5">
+ <i18n.Translate>
+ Here you can specify a default value for fields that are not
+ fixed. Default values can be edited by the customer before the
+ payment.
+ </i18n.Translate>
+ </p>
+
+ <p></p>
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={
+ fixedAmount
+ ? i18n.str`Fixed amount`
+ : i18n.str`Default amount`
+ }
+ readonly={fixedAmount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ readonly={fixedSummary}
+ label={
+ fixedSummary
+ ? i18n.str`Fixed summary`
+ : i18n.str`Default summary`
+ }
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <button
+ class="button is-info"
+ onClick={() => saveAsPDF(templateId)}
+ >
+ <i18n.Translate>Print</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={payTemplateUri}>{payTemplateUri}</a>
+ </pre>
+ </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/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
new file mode 100644
index 000000000..7db7478f7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -0,0 +1,80 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { QrPage } from "./QrPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+
+export default function TemplateQrPage({
+ tid,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
new file mode 100644
index 000000000..b578d4664
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,254 @@
+/*
+ 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)
+ */
+
+import {
+ Amounts,
+ MerchantTemplateContractDetails,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
+
+enum Steps {
+ BOTH_FIXED,
+ FIXED_PRICE,
+ FIXED_SUMMARY,
+ NON_FIXED,
+}
+
+type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ template: Entity;
+}
+
+export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { url: backendURL } = useBackendContext()
+
+ const intialStep =
+ template.template_contract?.amount === undefined && template.template_contract?.summary === undefined
+ ? Steps.NON_FIXED
+ : template.template_contract?.summary === undefined
+ ? Steps.FIXED_PRICE
+ : template.template_contract?.amount === undefined
+ ? Steps.FIXED_SUMMARY
+ : Steps.BOTH_FIXED;
+
+ const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep });
+
+ const parsedPrice = !state.template_contract?.amount
+ ? undefined
+ : Amounts.parse(state.template_contract?.amount);
+
+ const errors: FormErrors<Entity> = {
+ template_description: !state.template_description
+ ? i18n.str`should not be empty`
+ : undefined,
+ template_contract: !state.template_contract
+ ? undefined
+ : undefinedIfEmpty({
+ amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.amount
+ ? i18n.str`required`
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
+ summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ ? undefined
+ : !state.template_contract?.summary
+ ? i18n.str`required`
+ : undefined,
+ minimum_age:
+ state.template_contract.minimum_age < 0
+ ? i18n.str`should be greater that 0`
+ : undefined,
+ pay_duration: !state.template_contract.pay_duration
+ ? i18n.str`can't be empty`
+ : state.template_contract.pay_duration.d_us === "forever"
+ ? undefined
+ : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (state.template_contract) {
+ if (state.type === Steps.NON_FIXED) {
+ delete state.template_contract.amount;
+ delete state.template_contract.summary;
+ } else if (state.type === Steps.FIXED_SUMMARY) {
+ delete state.template_contract.amount;
+ } else if (state.type === Steps.FIXED_PRICE) {
+ delete state.template_contract.summary;
+ }
+ }
+ delete state.type
+ return onUpdate(state as any);
+ };
+
+
+ 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">
+ {backendURL}/templates/{template.id}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputWithAddon<Entity>
+ name="id"
+ addonBefore={`templates/`}
+ readonly
+ label={i18n.str`Identifier`}
+ tooltip={i18n.str`Name of the template in URLs.`}
+ />
+
+ <Input<Entity>
+ name="template_description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <InputTab
+ name="type"
+ label={i18n.str`Type`}
+ help={(() => {
+ switch (state.type) {
+ case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
+ case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
+ case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
+ case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ }
+ })()}
+ tooltip={i18n.str`Define what the user be allowed to modify`}
+ values={[
+ Steps.NON_FIXED,
+ Steps.FIXED_PRICE,
+ Steps.FIXED_SUMMARY,
+ Steps.BOTH_FIXED,
+ ]}
+ toStr={(v: Steps): string => {
+ switch (v) {
+ case Steps.NON_FIXED: return i18n.str`Simple`
+ case Steps.FIXED_PRICE: return i18n.str`With price`
+ case Steps.FIXED_SUMMARY: return i18n.str`With summary`
+ case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ }
+ }}
+ />
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ <Input
+ name="template_contract.summary"
+ inputType="multiline"
+ label={i18n.str`Fixed summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ : undefined}
+ {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ <InputCurrency
+ name="template_contract.amount"
+ label={i18n.str`Fixed price`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ : undefined}
+ <InputNumber
+ name="template_contract.minimum_age"
+ label={i18n.str`Minimum age`}
+ help=""
+ tooltip={i18n.str`Is this contract restricted to some age?`}
+ />
+ <InputDuration
+ name="template_contract.pay_duration"
+ label={i18n.str`Payment timeout`}
+ help=""
+ tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
new file mode 100644
index 000000000..3adca45db
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateTemplate({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ template={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateTemplate(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
new file mode 100644
index 000000000..13576d94d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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)
+ */
+
+import { UsePage as TestedComponent } from "./UsePage.js";
+
+export default {
+ title: "Pages/Templates/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
new file mode 100644
index 000000000..983804d3e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -0,0 +1,143 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+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({ id, template, onCreateOrder, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ amount: template.template_contract.amount,
+ summary: template.template_contract.summary,
+ });
+
+ const errors: FormErrors<Entity> = {
+ amount:
+ !template.template_contract.amount && !state.amount
+ ? i18n.str`Amount is required`
+ : undefined,
+ summary:
+ !template.template_contract.summary && !state.summary
+ ? i18n.str`Order summary is required`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ if (template.template_contract.amount) {
+ delete state.amount;
+ }
+ if (template.template_contract.summary) {
+ delete state.summary;
+ }
+ return onCreateOrder(state as any);
+ };
+
+ 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" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ readonly={!!template.template_contract.amount}
+ tooltip={i18n.str`Amount of the order`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Order summary`}
+ readonly={!!template.template_contract.summary}
+ tooltip={i18n.str`Title of the order to be shown to the customer`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
new file mode 100644
index 000000000..ed1242ef5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -0,0 +1,101 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useTemplateAPI,
+ useTemplateDetails,
+} from "../../../../hooks/templates.js";
+import { Notification } from "../../../../utils/types.js";
+import { UsePage } from "./UsePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onOrderCreated: (id: string) => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+
+export default function TemplateUsePage({
+ tid,
+ onOrderCreated,
+ onBack,
+ onLoadError,
+ onNotFound,
+ onUnauthorized,
+}: Props): VNode {
+ const { createOrderFromTemplate } = useTemplateAPI();
+ const result = useTemplateDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <UsePage
+ template={result.data}
+ id={tid}
+ onBack={onBack}
+ onCreateOrder={(
+ request: MerchantBackend.Template.UsingTemplateDetails,
+ ) => {
+ return createOrderFromTemplate(tid, request)
+ .then((res) => onOrderCreated(res.data.order_id))
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
new file mode 100644
index 000000000..d22a9e4d4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -0,0 +1,183 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import { FormProvider } from "../../../components/form/FormProvider.js";
+import { Input } from "../../../components/form/Input.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken } from "../../../declaration.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+
+interface Props {
+ instanceId: string;
+ hasToken: boolean | undefined;
+ onClearToken: (c: AccessToken | undefined) => void;
+ onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
+ onBack?: () => void;
+}
+
+export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const errors = {
+ old_token: hasToken && !form.old_token
+ ? i18n.str`you need your access token to perform the operation`
+ : undefined,
+ new_token: !form.new_token
+ ? i18n.str`cannot be empty`
+ : form.new_token === form.old_token
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`;
+
+ async function submitForm() {
+ if (hasErrors) return;
+ const ot = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined;
+ const nt = `secret-token:${form.new_token}` as AccessToken;
+ onNewToken(ot, nt)
+ }
+
+ 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">
+ {text}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ {!hasToken &&
+ <NotificationCard
+ notification={{
+ message: i18n.str`This instance doesn't have authentication token.`,
+ description: i18n.str`You can leave it empty if there is another layer of security.`,
+ type: "WARN",
+ }}
+ />
+ }
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ <Fragment>
+ {hasToken && (
+ <Fragment>
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Current access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button"
+ onClick={() => {
+ if (hasToken) {
+ const ot = `secret-token:${form.old_token}` as AccessToken;
+ onClearToken(ot)
+ } else {
+ onClearToken(undefined)
+ }
+ }}
+ >
+ <i18n.Translate>Clear token</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+ )}
+
+
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </Fragment>
+ </FormProvider>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm change</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
new file mode 100644
index 000000000..22365c9e1
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
@@ -0,0 +1,106 @@
+/*
+ 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/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Loading } from "../../../components/exception/loading.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
+import { DetailPage } from "./DetailPage.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { Notification } from "../../../utils/types.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onChange: () => void;
+ onNotFound: () => VNode;
+ onCancel: () => void;
+}
+
+export default function Token({
+ onLoadError,
+ onChange,
+ onUnauthorized,
+ onNotFound,
+ onCancel,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { clearAccessToken, setNewAccessToken } = useInstanceAPI();
+ const { id } = useInstanceContext();
+ const result = useInstanceDetails()
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ const hasToken = result.data.auth.method === "token"
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <DetailPage
+ instanceId={id}
+ onBack={onCancel}
+ hasToken={hasToken}
+ onClearToken={async (currentToken): Promise<void> => {
+ try {
+ await clearAccessToken(currentToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ onNewToken={async (currentToken, newToken): Promise<void> => {
+ try {
+ await setNewAccessToken(currentToken, newToken);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
new file mode 100644
index 000000000..5f0f56f2d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Token",
+ component: TestedComponent,
+};
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
new file mode 100644
index 000000000..64b67335c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
@@ -0,0 +1,45 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Transfer/Create",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
new file mode 100644
index 000000000..13f5f3c12
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { useConfigContext } from "../../../../context/config.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ CROCKFORD_BASE32_REGEX,
+ URL_REGEX,
+} from "../../../../utils/constants.js";
+
+type Entity = MerchantBackend.Transfers.TransferInformation;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ accounts: string[];
+}
+
+export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { currency } = useConfigContext();
+
+ const [state, setState] = useState<Partial<Entity>>({
+ wtid: "",
+ // payto_uri: ,
+ // exchange_url: 'http://exchange.taler:8081/',
+ credit_amount: ``,
+ });
+
+ const errors: FormErrors<Entity> = {
+ wtid: !state.wtid
+ ? i18n.str`cannot be empty`
+ : !CROCKFORD_BASE32_REGEX.test(state.wtid)
+ ? i18n.str`check the id, does not look valid`
+ : state.wtid.length !== 52
+ ? i18n.str`should have 52 characters, current ${state.wtid.length}`
+ : undefined,
+ payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined,
+ credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined,
+ exchange_url: !state.exchange_url
+ ? i18n.str`cannot be empty`
+ : !URL_REGEX.test(state.exchange_url)
+ ? i18n.str`URL doesn't have the right format`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Credited bank account`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`Bank account of the merchant where the payment was received`}
+ />
+ <Input<Entity>
+ name="wtid"
+ label={i18n.str`Wire transfer ID`}
+ help=""
+ tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`}
+ />
+ <Input<Entity>
+ name="exchange_url"
+ label={i18n.str`Exchange URL`}
+ tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`}
+ help="http://exchange.taler:8081/"
+ />
+ <InputCurrency<Entity>
+ name="credit_amount"
+ label={i18n.str`Amount credited`}
+ tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
new file mode 100644
index 000000000..25551a031
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -0,0 +1,68 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useTransferAPI } from "../../../../hooks/transfer.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+export type Entity = MerchantBackend.Transfers.TransferInformation;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
+ const { informTransfer } = useTransferAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ accounts={accounts}
+ onCreate={(request: MerchantBackend.Transfers.TransferInformation) => {
+ return informTransfer(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform transfer`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
new file mode 100644
index 000000000..92b3f9853
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
@@ -0,0 +1,93 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Transfer/List",
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: "onCreate" },
+ onDelete: { action: "onDelete" },
+ onLoadMoreBefore: { action: "onLoadMoreBefore" },
+ onLoadMoreAfter: { action: "onLoadMoreAfter" },
+ onShowAll: { action: "onShowAll" },
+ onShowVerified: { action: "onShowVerified" },
+ onShowUnverified: { action: "onShowUnverified" },
+ onChangePayTo: { action: "onChangePayTo" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ transfers: [
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ {
+ exchange_url: "http://exchange.url/",
+ credit_amount: "TESTKUDOS:10",
+ payto_uri: "payto//x-taler-bank/bank:8080/account",
+ transfer_serial_id: 123123123,
+ wtid: "!@KJELQKWEJ!L@K#!J@",
+ confirmed: true,
+ execution_time: {
+ t_s: new Date().getTime() / 1000,
+ },
+ verified: false,
+ },
+ ],
+ accounts: ["payto://x-taler-bank/bank/some_account"],
+});
+export const Empty = createExample(TestedComponent, {
+ transfers: [],
+ accounts: [],
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
new file mode 100644
index 000000000..02b12c4c2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -0,0 +1,134 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { FormProvider } from "../../../../components/form/FormProvider.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ transfers: MerchantBackend.Transfers.TransferDetails[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onShowAll: () => void;
+ onShowVerified: () => void;
+ onShowUnverified: () => void;
+ isVerifiedTransfers?: boolean;
+ isNonVerifiedTransfers?: boolean;
+ isAllTransfers?: boolean;
+ accounts: string[];
+ onChangePayTo: (p?: string) => void;
+ payTo?: string;
+ onCreate: () => void;
+ onDelete: () => void;
+}
+
+export function ListPage({
+ payTo,
+ onChangePayTo,
+ transfers,
+ onCreate,
+ onDelete,
+ accounts,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+ isAllTransfers,
+ isNonVerifiedTransfers,
+ isVerifiedTransfers,
+ onShowAll,
+ onShowUnverified,
+ onShowVerified,
+}: Props): VNode {
+ const form = { payto_uri: payTo };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-10">
+ <FormProvider
+ object={form}
+ valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)}
+ >
+ <InputSelector
+ name="payto_uri"
+ label={i18n.str`Account URI`}
+ values={accounts}
+ placeholder={i18n.str`Select one account`}
+ tooltip={i18n.str`filter by account address`}
+ />
+ </FormProvider>
+ </div>
+ <div class="column" />
+ </div>
+ <div class="tabs">
+ <ul>
+ <li class={isAllTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`}
+ >
+ <a onClick={onShowVerified}>
+ <i18n.Translate>Verified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isNonVerifiedTransfers ? "is-active" : ""}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`only show wire transfers claimed by the exchange`}
+ >
+ <a onClick={onShowUnverified}>
+ <i18n.Translate>Unverified</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <CardTable
+ transfers={transfers.map((o) => ({
+ ...o,
+ id: String(o.transfer_serial_id),
+ }))}
+ accounts={accounts}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
new file mode 100644
index 000000000..b6b1cf328
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -0,0 +1,229 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
+
+type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
+
+interface Props {
+ transfers: Entity[];
+ onDelete: (id: Entity) => void;
+ onCreate: () => void;
+ accounts: string[];
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ transfers,
+ onCreate,
+ onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-arrow-left-right" />
+ </span>
+ <i18n.Translate>Transfers</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new transfer`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {transfers.length > 0 ? (
+ <Table
+ instances={transfers}
+ onDelete={onDelete}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (id: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfers before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer transfers</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Credit</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Exchange URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Confirmed</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Verified</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Executed at</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.id}>
+ <td>{i.id}</td>
+ <td>{i.credit_amount}</td>
+ <td>{i.payto_uri}</td>
+ <td>{i.exchange_url}</td>
+ <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td>
+ <td>
+ {i.execution_time
+ ? i.execution_time.t_s == "never"
+ ? i18n.str`never`
+ : format(
+ i.execution_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
+ : i18n.str`unknown`}
+ </td>
+ <td>
+ {i.verified === undefined ? (
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected transfer from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more transfer after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older transfers</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no transfer yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
new file mode 100644
index 000000000..0fdbb9bc3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -0,0 +1,118 @@
+/*
+ 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)
+ */
+
+import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useInstanceDetails } from "../../../../hooks/instance.js";
+import { useInstanceTransfers } from "../../../../hooks/transfer.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+}
+interface Form {
+ verified?: "yes" | "no";
+ payto_uri?: string;
+}
+
+export default function ListTransfer({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onNotFound,
+}: Props): VNode {
+ const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
+
+ const [position, setPosition] = useState<string | undefined>(undefined);
+
+ const instance = useInstanceBankAccounts();
+ const accounts = !instance.ok
+ ? []
+ : instance.data.accounts.map((a) => a.payto_uri);
+ const [form, setForm] = useState<Form>({ payto_uri: "" });
+
+ const shoulUseDefaultAccount = accounts.length === 1
+ useEffect(() => {
+ if (shoulUseDefaultAccount) {
+ setForm({...form, payto_uri: accounts[0]})
+ }
+ }, [shoulUseDefaultAccount])
+
+ const isVerifiedTransfers = form.verified === "yes";
+ const isNonVerifiedTransfers = form.verified === "no";
+ const isAllTransfers = form.verified === undefined;
+
+ const result = useInstanceTransfers(
+ {
+ position,
+ payto_uri: form.payto_uri === "" ? undefined : form.payto_uri,
+ verified: form.verified,
+ },
+ (id) => setPosition(id),
+ );
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <ListPage
+ accounts={accounts}
+ transfers={result.data.transfers}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onDelete={() => {
+ null;
+ }}
+ // position={position} setPosition={setPosition}
+ onShowAll={() => setFilter(undefined)}
+ onShowUnverified={() => setFilter("no")}
+ onShowVerified={() => setFilter("yes")}
+ isAllTransfers={isAllTransfers}
+ isVerifiedTransfers={isVerifiedTransfers}
+ isNonVerifiedTransfers={isNonVerifiedTransfers}
+ payTo={form.payto_uri}
+ onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))}
+ />
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
new file mode 100644
index 000000000..84cc95e72
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
@@ -0,0 +1,26 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+
+export default function UpdateTransfer(): VNode {
+ return <div>order transfer page</div>;
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
new file mode 100644
index 000000000..817a7025c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
@@ -0,0 +1,59 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Instance/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
+}
+
+export const Example = createExample(TestedComponent, {
+ selected: {
+ name: "name",
+ auth: { method: "external" },
+ address: {},
+ user_type: "business",
+ use_stefan: true,
+ jurisdiction: {},
+ default_pay_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000, //one second
+ },
+ merchant_pub: "ASDWQEKASJDKSADJ",
+ },
+});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
new file mode 100644
index 000000000..a27a0cb06
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -0,0 +1,176 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../components/form/FormProvider.js";
+import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { undefinedIfEmpty } from "../../../utils/table.js";
+import { Duration } from "@gnu-taler/taler-util";
+
+export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+ default_pay_delay: Duration,
+ default_wire_transfer_delay: Duration,
+};
+
+//MerchantBackend.Instances.InstanceAuthConfigurationMessage
+interface Props {
+ onUpdate: (d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void;
+ selected: MerchantBackend.Instances.QueryInstancesResponse;
+ isLoading: boolean;
+ onBack: () => void;
+}
+
+function convert(
+ from: MerchantBackend.Instances.QueryInstancesResponse,
+): Entity {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = from;
+
+ const defaults = {
+ use_stefan: false,
+ default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay),
+ };
+ return { ...defaults, ...rest };
+}
+
+export function UpdatePage({
+ onUpdate,
+ selected,
+ onBack,
+}: Props): VNode {
+ const { id } = useInstanceContext();
+
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ name: !value.name ? i18n.str`required` : undefined,
+ user_type: !value.user_type
+ ? i18n.str`required`
+ : value.user_type !== "business" && value.user_type !== "individual"
+ ? i18n.str`should be business or individual`
+ : undefined,
+ default_pay_delay: !value.default_pay_delay
+ ? i18n.str`required`
+ : !!value.default_wire_transfer_delay &&
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
+ i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ default_wire_transfer_delay: !value.default_wire_transfer_delay
+ ? i18n.str`required`
+ : undefined,
+ address: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ jurisdiction: undefinedIfEmpty({
+ address_lines:
+ value.address?.address_lines && value.address?.address_lines.length > 7
+ ? i18n.str`max 7 lines`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = async (): Promise<void> => {
+ const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>;
+ const result: MerchantBackend.Instances.InstanceReconfigurationMessage = {
+ default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay),
+ ...rest,
+ }
+ await onUpdate(result);
+ };
+ // const [active, setActive] = useState(false);
+
+ 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>Instance id</i18n.Translate>: <b>{id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <DefaultInstanceFormFields showId={false} />
+ </FormProvider>
+
+ <div class="buttons is-right mt-4">
+ <button
+ class="button"
+ onClick={onBack}
+ data-tooltip="cancel operation"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <AsyncButton
+ onClick={submit}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ disabled={hasErrors}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
new file mode 100644
index 000000000..e44cf5c0f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
@@ -0,0 +1,118 @@
+/*
+ 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/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ HttpResponse,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { useInstanceContext } from "../../../context/instance.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import {
+ useInstanceAPI,
+ useInstanceDetails,
+ useManagedInstanceDetails,
+ useManagementAPI,
+} from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export interface Props {
+ onBack: () => void;
+ onConfirm: () => void;
+
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void;
+}
+
+export default function Update(props: Props): VNode {
+ const { updateInstance } = useInstanceAPI();
+ const result = useInstanceDetails();
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+export function AdminUpdate(props: Props & { instanceId: string }): VNode {
+ const { updateInstance } = useManagementAPI(
+ props.instanceId,
+ );
+ const result = useManagedInstanceDetails(props.instanceId);
+ return CommonUpdate(props, result, updateInstance, );
+}
+
+function CommonUpdate(
+ {
+ onBack,
+ onConfirm,
+ onLoadError,
+ onNotFound,
+ onUpdateError,
+ onUnauthorized,
+ }: Props,
+ result: HttpResponse<
+ MerchantBackend.Instances.QueryInstancesResponse,
+ MerchantBackend.ErrorDetail
+ >,
+ updateInstance: any,
+): VNode {
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ onBack={onBack}
+ isLoading={false}
+ selected={result.data}
+ onUpdate={(
+ d: MerchantBackend.Instances.InstanceReconfigurationMessage,
+ ): Promise<void> => {
+ return updateInstance(d)
+ .then(onConfirm)
+ .catch((error: Error) =>
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ );
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
new file mode 100644
index 000000000..4857ede97
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
+
+export default {
+ title: "Pages/Webhooks/Create",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
new file mode 100644
index 000000000..434d69412
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -0,0 +1,183 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function CreatePage({ onCreate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>({});
+
+ const errors: FormErrors<Entity> = {
+ webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
+ event_type: !state.event_type ? i18n.str`required`
+ : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"`
+ : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="webhook_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Webhook ID to use`}
+ />
+ <InputSelector
+ name="event_type"
+ label={i18n.str`Event`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`pay`,
+ i18n.str`refund`,
+ ]}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <InputSelector
+ name="http_method"
+ label={i18n.str`Method`}
+ values={[
+ i18n.str`Choose one...`,
+ i18n.str`GET`,
+ i18n.str`POST`,
+ i18n.str`PUT`,
+ i18n.str`PATCH`,
+ i18n.str`HEAD`,
+ ]}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+
+ <p>
+ The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string
+ between <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;</pre> and <pre style={{ display: "inline", padding: 0 }}>&#125;&#125;</pre> will
+ be replaced with replaced with the value of the correspoding variable.
+ </p>
+ <p>
+ For example <pre style={{ display: "inline", padding: 0 }}>&#123;&#123;contract_terms.amount&#125;&#125;</pre> will be replaced
+ with the the order's price
+ </p>
+ <p>
+ The short list of variables are:
+ </p>
+ <div class="menu">
+
+ <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}>
+ <li><b>contract_terms.summary:</b> order's description </li>
+ <li><b>contract_terms.amount:</b> order's price </li>
+ <li><b>order_id:</b> order's unique identification </li>
+ {state.event_type === "refund" && <Fragment>
+ <li><b>refund_amout:</b> the amount that was being refunded</li>
+ <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li>
+ <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li>
+ </Fragment>}
+ </ul>
+ </div>
+ {/* <Input<Entity>
+ name="header_template"
+ label={i18n.str`Http header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ /> */}
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Http body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
new file mode 100644
index 000000000..924e6d9b8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -0,0 +1,61 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { CreatePage } from "./CreatePage.js";
+
+export type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
+ const { createWebhook } = useWebhookAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => {
+ return createWebhook(request)
+ .then(() => onConfirm())
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not inform template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
new file mode 100644
index 000000000..702e9ba4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+import { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Templates/List",
+ component: TestedComponent,
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
new file mode 100644
index 000000000..87e221e3c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { CardTable } from "./Table.js";
+
+export interface Props {
+ webhooks: MerchantBackend.Webhooks.WebhookEntry[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+ onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
+}
+
+export function ListPage({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ webhooks={webhooks.map((o) => ({
+ ...o,
+ id: String(o.webhook_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
new file mode 100644
index 000000000..42a179d2c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -0,0 +1,218 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookEntry;
+
+interface Props {
+ webhooks: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ webhooks,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Webhooks</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new webhooks`}
+ >
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small">
+ <i class="mdi mdi-plus mdi-36px" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {webhooks.length > 0 ? (
+ <Table
+ instances={webhooks}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ rowSelectionHandler: StateUpdater<string[]>;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
+ return (prev: T[]): T[] =>
+ prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
+}
+
+function Table({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {hasMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks before the first one`}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer webhooks</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Event type</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.webhook_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.webhook_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.event_type}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected webhook from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ {/* <button
+ class="button is-info is-small has-tooltip-left"
+ data-tooltip={i18n.str`test webhook`}
+ onClick={() => onNewOrder(i)}
+ >
+ Test
+ </button> */}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {hasMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more webhooks after the last one`}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older webhooks</i18n.Translate>
+ </button>
+ )}
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="content has-text-grey has-text-centered">
+ <p>
+ <span class="icon is-large">
+ <i class="mdi mdi-emoticon-sad mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>
+ There is no webhooks yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
new file mode 100644
index 000000000..a6f6f1511
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -0,0 +1,109 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import {
+ useInstanceWebhooks,
+ useWebhookAPI,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListWebhooks({
+ onUnauthorized,
+ onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+}: Props): VNode {
+ const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { deleteWebhook } = useWebhookAPI();
+ const result = useInstanceWebhooks({ position }, (id) => setPosition(id));
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ webhooks={result.data.webhooks}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.webhook_id);
+ }}
+ onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) =>
+ deleteWebhook(e.webhook_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`webhook delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the webhook`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
new file mode 100644
index 000000000..8d07cb31f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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)
+ */
+
+import { h, VNode, FunctionalComponent } from "preact";
+import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Templates/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
new file mode 100644
index 000000000..76a23b6e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -0,0 +1,146 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ webhook: Entity;
+}
+const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
+
+export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(webhook);
+
+ const errors: FormErrors<Entity> = {
+ event_type: !state.event_type ? i18n.str`required` : undefined,
+ http_method: !state.http_method
+ ? i18n.str`required`
+ : !validMethod.includes(state.http_method)
+ ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ : undefined,
+ url: !state.url ? i18n.str`required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ 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">
+ Webhook: <b>{webhook.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="event_type"
+ label={i18n.str`Event`}
+ tooltip={i18n.str`The event of the webhook: why the webhook is used`}
+ />
+ <Input<Entity>
+ name="http_method"
+ label={i18n.str`Method`}
+ tooltip={i18n.str`Method used by the webhook`}
+ />
+ <Input<Entity>
+ name="url"
+ label={i18n.str`URL`}
+ tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
+ />
+ <Input<Entity>
+ name="header_template"
+ label={i18n.str`Header`}
+ inputType="multiline"
+ tooltip={i18n.str`Header template of the webhook`}
+ />
+ <Input<Entity>
+ name="body_template"
+ inputType="multiline"
+ label={i18n.str`Body`}
+ tooltip={i18n.str`Body template by the webhook`}
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
new file mode 100644
index 000000000..3f723ed87
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -0,0 +1,99 @@
+/*
+ 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)
+ */
+
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+import {
+ useWebhookAPI,
+ useWebhookDetails,
+} from "../../../../hooks/webhooks.js";
+import { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ tid: string;
+}
+export default function UpdateWebhook({
+ tid,
+ onConfirm,
+ onBack,
+ onUnauthorized,
+ onNotFound,
+ onLoadError,
+}: Props): VNode {
+ const { updateWebhook } = useWebhookAPI();
+ const result = useWebhookDetails(tid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ return onLoadError(result);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ webhook={{ ...result.data, id: tid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateWebhook(tid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/login/index.tsx b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
new file mode 100644
index 000000000..1c98b7c9b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
@@ -0,0 +1,202 @@
+/*
+ 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)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useInstanceContext } from "../../context/instance.js";
+import { AccessToken, LoginToken } from "../../declaration.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
+
+interface Props {
+ onConfirm: (token: LoginToken | undefined) => void;
+}
+
+function normalizeToken(r: string): AccessToken {
+ return `secret-token:${r}` as AccessToken;
+}
+
+export function LoginPage({ onConfirm }: Props): VNode {
+ const { url: backendURL } = useBackendContext();
+ const { admin, id } = useInstanceContext();
+ const { requestNewLoginToken } = useCredentialsChecker();
+ const [token, setToken] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+
+ const doLogin = useCallback(async function doLoginImpl() {
+ const secretToken = normalizeToken(token);
+ const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}`
+ const result = await requestNewLoginToken(baseUrl, secretToken);
+ if (result.valid) {
+ const { token, expiration } = result
+ onConfirm({ token, expiration });
+ } else {
+ onConfirm(undefined);
+ }
+ }, [id, token])
+
+ if (admin && id !== "default") {
+ //admin trying to access another instance
+ return (<div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <p>
+ <i18n.Translate>Need the access token for the instance.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>)
+ }
+
+ return (
+ <div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <i18n.Translate>Please enter your access token.</i18n.Translate>
+
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <div />
+ <AsyncButton
+ type="is-info"
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
+ const [running, setRunning] = useState(false)
+ return <button class={"button " + type} disabled={disabled || running} onClick={() => {
+ setRunning(true)
+ onClick().then(() => {
+ setRunning(false)
+ }).catch(() => {
+ setRunning(false)
+ })
+ }}>
+ {children}
+ </button>
+}
+
+
diff --git a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
new file mode 100644
index 000000000..061a67025
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
@@ -0,0 +1,34 @@
+/*
+ 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)
+ */
+
+import { h, VNode } from "preact";
+import { Link } from "preact-router";
+
+export default function NotFoundPage(): VNode {
+ return (
+ <div>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
new file mode 100644
index 000000000..87bd2fa39
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
@@ -0,0 +1,112 @@
+import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
+import { InputSelector } from "../../components/form/InputSelector.js";
+import { InputToggle } from "../../components/form/InputToggle.js";
+import { LangSelector } from "../../components/menu/LangSelector.js";
+import { Settings, useSettings } from "../../hooks/useSettings.js";
+
+function getBrowserLang(): string | undefined {
+ if (typeof window === "undefined") return undefined;
+ if (window.navigator.languages) return window.navigator.languages[0];
+ if (window.navigator.language) return window.navigator.language;
+ return undefined;
+}
+
+export function Settings({ onClose }: { onClose?: () => void }): VNode {
+ const { i18n } = useTranslationContext()
+ const borwserLang = getBrowserLang()
+ const { update } = useLang()
+
+ const [value, updateValue] = useSettings()
+ const errors: FormErrors<Settings> = {
+ }
+
+ function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
+ const next = s(value)
+ const v: Settings = {
+ advanceOrderMode: next.advanceOrderMode ?? false,
+ dateFormat: next.dateFormat ?? "ymd"
+ }
+ updateValue(v)
+ }
+
+ return <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div>
+
+ <FormProvider<Settings>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Language</i18n.Translate>
+ <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field field-body has-addons is-flex-grow-3">
+ <LangSelector />
+ &nbsp;
+ {borwserLang !== undefined && <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-2"
+ onClick={(e) => {
+ update(borwserLang.substring(0, 2))
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>}
+ </div>
+ </div>
+ <InputToggle<Settings>
+ label={i18n.str`Advance order creation`}
+ tooltip={i18n.str`Shows more options in the order creation form`}
+ name="advanceOrderMode"
+ />
+ <InputSelector<Settings>
+ name="dateFormat"
+ label={i18n.str`Date format`}
+ expand={true}
+ help={
+ value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : ""
+ }
+ toStr={(e) => {
+ if (e === "ymd") return "year month day"
+ if (e === "mdy") return "month day year"
+ if (e === "dmy") return "day month year"
+ return "choose one"
+ }}
+ values={[
+ "ymd",
+ "mdy",
+ "dmy",
+ ]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section >
+ {onClose &&
+ <section class="section is-main-section">
+ <button
+ class="button"
+ onClick={onClose}
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ }
+ </div >
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/schemas/index.ts b/packages/auditor-backoffice-ui/src/schemas/index.ts
new file mode 100644
index 000000000..380466e13
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/schemas/index.ts
@@ -0,0 +1,245 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { isAfter, isFuture } from "date-fns";
+import * as yup from "yup";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants.js";
+import { Amounts } from "@gnu-taler/taler-util";
+
+yup.setLocale({
+ mixed: {
+ default: "field_invalid",
+ },
+ number: {
+ min: ({ min }: any) => ({ key: "field_too_short", values: { min } }),
+ max: ({ max }: any) => ({ key: "field_too_big", values: { max } }),
+ },
+});
+
+function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean {
+ return !!values && values.every((v) => v && PAYTO_REGEX.test(v));
+}
+
+function currencyWithAmountIsValid(value?: string): boolean {
+ return !!value && Amounts.parse(value) !== undefined;
+}
+function currencyGreaterThan0(value?: string) {
+ if (value) {
+ try {
+ const [, amount] = value.split(":");
+ const intAmount = parseInt(amount, 10);
+ return intAmount > 0;
+ } catch {
+ return false;
+ }
+ }
+ return true;
+}
+
+export const InstanceSchema = yup.object().shape({
+ id: yup.string().required().meta({ type: "url" }),
+ name: yup.string().required(),
+ auth: yup.object().shape({
+ method: yup.string().matches(/^(external|token)$/),
+ token: yup.string().optional().nullable(),
+ }),
+ payto_uris: yup
+ .array()
+ .of(yup.string())
+ .min(1)
+ .meta({ type: "array" })
+ .test("payto", "{path} is not valid", listOfPayToUrisAreValid),
+ default_max_deposit_fee: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_max_wire_fee: yup
+ .string()
+ .required()
+ .test("amount", "{path} is not valid", currencyWithAmountIsValid)
+ .meta({ type: "amount" }),
+ default_wire_fee_amortization: yup.number().required(),
+ address: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
+ jurisdiction: yup
+ .object()
+ .shape({
+ country: yup.string().optional(),
+ address_lines: yup.array().of(yup.string()).max(7).optional(),
+ building_number: yup.string().optional(),
+ building_name: yup.string().optional(),
+ street: yup.string().optional(),
+ post_code: yup.string().optional(),
+ town_location: yup.string().optional(),
+ town: yup.string(),
+ district: yup.string().optional(),
+ country_subdivision: yup.string().optional(),
+ })
+ .meta({ type: "group" }),
+ // default_pay_delay: yup.object()
+ // .shape({ d_us: yup.number() })
+ // .required()
+ // .meta({ type: 'duration' }),
+ // .transform(numberToDuration),
+ default_wire_transfer_delay: yup
+ .object()
+ .shape({ d_us: yup.number() })
+ .required()
+ .meta({ type: "duration" }),
+ // .transform(numberToDuration),
+});
+
+export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]);
+export const InstanceCreateSchema = InstanceSchema.clone();
+
+export const AuthorizeRewardSchema = yup.object().shape({
+ justification: yup.string().required(),
+ amount: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .test("amount_positive", "the amount is not valid", currencyGreaterThan0),
+ next_url: yup.string().required(),
+});
+
+const stringIsValidJSON = (value?: string) => {
+ const p = value?.trim();
+ if (!p) return true;
+ try {
+ JSON.parse(p);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export const OrderCreateSchema = yup.object().shape({
+ pricing: yup
+ .object()
+ .required()
+ .shape({
+ summary: yup.string().ensure().required(),
+ order_price: yup
+ .string()
+ .ensure()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid)
+ .test(
+ "amount_positive",
+ "the amount should be greater than 0",
+ currencyGreaterThan0,
+ ),
+ }),
+ // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON),
+ payments: yup
+ .object()
+ .required()
+ .shape({
+ refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ pay_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ auto_refund_deadline: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ delivery_date: yup
+ .date()
+ .test("future", "should be in the future", (d) =>
+ d ? isFuture(d) : true,
+ ),
+ })
+ .test("payment", "dates", (d) => {
+ if (
+ d.pay_deadline &&
+ d.refund_deadline &&
+ isAfter(d.refund_deadline, d.pay_deadline)
+ ) {
+ return new yup.ValidationError(
+ "pay deadline should be greater than refund",
+ "asd",
+ "payments.pay_deadline",
+ );
+ }
+ return true;
+ }),
+});
+
+export const ProductCreateSchema = yup.object().shape({
+ product_id: yup.string().ensure().required(),
+ description: yup.string().required(),
+ unit: yup.string().ensure().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
+ minimum_age: yup.number().optional().min(0),
+});
+
+export const ProductUpdateSchema = yup.object().shape({
+ description: yup.string().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+ stock: yup.object({}).optional(),
+ minimum_age: yup.number().optional().min(0),
+});
+
+export const TaxSchema = yup.object().shape({
+ name: yup.string().required().ensure(),
+ tax: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
+
+export const NonInventoryProductSchema = yup.object().shape({
+ quantity: yup.number().required().positive(),
+ description: yup.string().required(),
+ unit: yup.string().ensure().required(),
+ price: yup
+ .string()
+ .required()
+ .test("amount", "the amount is not valid", currencyWithAmountIsValid),
+});
diff --git a/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss
new file mode 100644
index 000000000..aa75b9916
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/DurationPicker.scss
@@ -0,0 +1,70 @@
+.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/auditor-backoffice-ui/src/scss/_aside.scss b/packages/auditor-backoffice-ui/src/scss/_aside.scss
new file mode 100644
index 000000000..e0922093b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_aside.scss
@@ -0,0 +1,181 @@
+/*
+ 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)
+ */
+
+@include desktop {
+ html {
+ &.has-aside-left {
+ &.has-aside-expanded {
+ nav.navbar,
+ body {
+ padding-left: $aside-width;
+ }
+ }
+ aside.is-placed-left {
+ display: block;
+ }
+ }
+ }
+
+ aside.aside.is-expanded {
+ width: $aside-width;
+
+ .menu-list {
+ @include icon-with-update-mark($aside-icon-width);
+
+ span.menu-item-label {
+ display: inline-block;
+ }
+
+ li.is-active {
+ ul {
+ display: block;
+ }
+ }
+ }
+ }
+}
+
+aside.aside {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 40;
+ height: 100vh;
+ padding: 0;
+ box-shadow: $aside-box-shadow;
+ background: $aside-background-color;
+
+ .aside-tools {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ background-color: $aside-tools-background-color;
+ color: $aside-tools-color;
+ line-height: $navbar-height;
+ height: $navbar-height;
+ padding-left: $default-padding * 0.5;
+ flex: 1;
+
+ .icon {
+ margin-right: $default-padding * 0.5;
+ }
+ }
+
+ .menu-list {
+ li {
+ a {
+ &.has-dropdown-icon {
+ position: relative;
+ padding-right: $aside-icon-width;
+
+ .dropdown-icon {
+ position: absolute;
+ top: $size-base * 0.5;
+ right: 0;
+ }
+ }
+ }
+ ul {
+ display: none;
+ border-left: 0;
+ background-color: darken($base-color, 2.5%);
+ padding-left: 0;
+ margin: 0 0 $default-padding * 0.5;
+
+ li {
+ a {
+ padding: $default-padding * 0.5 0 $default-padding * 0.5
+ $default-padding * 0.5;
+ font-size: $aside-submenu-font-size;
+
+ &.has-icon {
+ padding-left: 0;
+ }
+ &.is-active {
+ &:not(:hover) {
+ background: transparent;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .menu-label {
+ padding: 0 $default-padding * 0.5;
+ margin-top: $default-padding * 0.5;
+ margin-bottom: $default-padding * 0.5;
+ }
+}
+
+@include touch {
+ nav.navbar {
+ @include transition(margin-left);
+ }
+ aside.aside {
+ @include transition(left);
+ }
+ html.has-aside-mobile-transition {
+ body {
+ overflow-x: hidden;
+ }
+ body,
+ nav.navbar {
+ width: 100vw;
+ }
+ aside.aside {
+ width: $aside-mobile-width;
+ display: block;
+ left: $aside-mobile-width * -1;
+
+ .image {
+ img {
+ max-width: $aside-mobile-width * 0.33;
+ }
+ }
+
+ .menu-list {
+ li.is-active {
+ ul {
+ display: block;
+ }
+ }
+ a {
+ @include icon-with-update-mark($aside-icon-width);
+
+ span.menu-item-label {
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+ div.has-aside-mobile-expanded {
+ nav.navbar {
+ margin-left: $aside-mobile-width;
+ }
+ aside.aside {
+ left: 0;
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_card.scss b/packages/auditor-backoffice-ui/src/scss/_card.scss
new file mode 100644
index 000000000..62db7f457
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_card.scss
@@ -0,0 +1,69 @@
+/*
+ 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)
+ */
+
+.card:not(:last-child) {
+ margin-bottom: $default-padding;
+}
+
+.card {
+ border-radius: $radius-large;
+ border: $card-border;
+
+ &.has-table {
+ .card-content {
+ padding: 0;
+ }
+ .b-table {
+ border-radius: $radius-large;
+ overflow: hidden;
+ }
+ }
+
+ &.is-card-widget {
+ .card-content {
+ padding: $default-padding * 0.5;
+ }
+ }
+
+ .card-header {
+ border-bottom: 1px solid $base-color-light;
+ }
+
+ .card-content {
+ hr {
+ margin-left: $card-content-padding * -1;
+ margin-right: $card-content-padding * -1;
+ }
+ }
+
+ .is-widget-icon {
+ .icon {
+ width: 5rem;
+ height: 5rem;
+ }
+ }
+
+ .is-widget-label {
+ .subtitle {
+ color: $grey;
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
new file mode 100644
index 000000000..34c40092b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
@@ -0,0 +1,259 @@
+/*
+ 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/>
+ */
+
+:root {
+ --primary-color: #3298dc;
+
+ --primary-text-color-dark: rgba(0, 0, 0, 0.87);
+ --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
+ --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
+
+ --primary-text-color-light: rgba(255, 255, 255, 0.87);
+ --secondary-text-color-light: rgba(255, 255, 255, 0.57);
+ --disabled-text-color-light: rgba(255, 255, 255, 0.13);
+
+ --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
+ --primary-card-color: #fff;
+ --primary-background-color: #f2f2f2;
+
+ --box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 1px 2px rgba(0, 0, 0, 0.24);
+ --box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
+ 0 3px 6px rgba(0, 0, 0, 0.23);
+ --box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
+ 0 6px 6px rgba(0, 0, 0, 0.23);
+ --box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
+ 0 10px 10px rgba(0, 0, 0, 0.22);
+}
+
+.datePicker {
+ text-align: left;
+ background: var(--primary-card-color);
+ border-radius: 3px;
+ z-index: 200;
+ position: fixed;
+ height: auto;
+ max-height: 90vh;
+ width: 90vw;
+ max-width: 448px;
+ transform-origin: top left;
+ transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
+ top: 50%;
+ left: 50%;
+ opacity: 0;
+ transform: scale(0) translate(-50%, -50%);
+ user-select: none;
+
+ &.datePicker--opened {
+ opacity: 1;
+ transform: scale(1) translate(-50%, -50%);
+ }
+
+ .datePicker--titles {
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ padding: 24px;
+ height: 100px;
+ background: var(--primary-color);
+
+ h2,
+ h3 {
+ cursor: pointer;
+ color: #fff;
+ line-height: 1;
+ padding: 0;
+ margin: 0;
+ font-size: 32px;
+ }
+
+ h3 {
+ color: rgba(255, 255, 255, 0.57);
+ font-size: 18px;
+ padding-bottom: 2px;
+ }
+ }
+
+ nav {
+ padding: 20px;
+ height: 56px;
+
+ h4 {
+ width: calc(100% - 60px);
+ text-align: center;
+ display: inline-block;
+ padding: 0;
+ font-size: 14px;
+ line-height: 24px;
+ margin: 0;
+ position: relative;
+ top: -9px;
+ color: var(--primary-text-color);
+ }
+
+ i {
+ cursor: pointer;
+ color: var(--secondary-text-color);
+ font-size: 26px;
+ user-select: none;
+ border-radius: 50%;
+
+ &:hover {
+ background: var(--disabled-text-color-dark);
+ }
+ }
+ }
+
+ .datePicker--scroll {
+ overflow-y: auto;
+ max-height: calc(90vh - 56px - 100px);
+ }
+
+ .datePicker--calendar {
+ padding: 0 20px;
+
+ .datePicker--dayNames {
+ width: 100%;
+ display: grid;
+ text-align: center;
+
+ // there's probably a better way to do this, but wanted to try out CSS grid
+ grid-template-columns:
+ calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
+ calc(100% / 7) calc(100% / 7) calc(100% / 7);
+
+ span {
+ color: var(--secondary-text-color-dark);
+ font-size: 14px;
+ line-height: 42px;
+ display: inline-grid;
+ }
+ }
+
+ .datePicker--days {
+ width: 100%;
+ display: grid;
+ text-align: center;
+ grid-template-columns:
+ calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
+ calc(100% / 7) calc(100% / 7) calc(100% / 7);
+
+ span {
+ color: var(--primary-text-color-dark);
+ line-height: 42px;
+ font-size: 14px;
+ display: inline-grid;
+ transition: color 0.22s;
+ height: 42px;
+ position: relative;
+ cursor: pointer;
+ user-select: none;
+ border-radius: 50%;
+
+ &::before {
+ content: "";
+ position: absolute;
+ z-index: -1;
+ height: 42px;
+ width: 42px;
+ left: calc(50% - 21px);
+ background: var(--primary-color);
+ border-radius: 50%;
+ transition: transform 0.22s, opacity 0.22s;
+ transform: scale(0);
+ opacity: 0;
+ }
+
+ &[disabled="true"] {
+ cursor: unset;
+ }
+
+ &.datePicker--today {
+ font-weight: 700;
+ }
+
+ &.datePicker--selected {
+ color: rgba(255, 255, 255, 0.87);
+
+ &:before {
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+
+ .datePicker--selectYear {
+ padding: 0 20px;
+ display: block;
+ width: 100%;
+ text-align: center;
+ max-height: 362px;
+
+ span {
+ display: block;
+ width: 100%;
+ font-size: 24px;
+ margin: 20px auto;
+ cursor: pointer;
+
+ &.selected {
+ font-size: 42px;
+ color: var(--primary-color);
+ }
+ }
+ }
+
+ div.datePicker--actions {
+ width: 100%;
+ padding: 8px;
+ text-align: right;
+
+ button {
+ margin-bottom: 0;
+ font-size: 15px;
+ cursor: pointer;
+ color: var(--primary-text-color);
+ border: none;
+ margin-left: 8px;
+ min-width: 64px;
+ line-height: 36px;
+ background-color: transparent;
+ appearance: none;
+ padding: 0 16px;
+ border-radius: 3px;
+ transition: background-color 0.13s;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ background-color: var(--disabled-text-color-dark);
+ }
+ }
+ }
+}
+
+.datePicker--background {
+ z-index: 199;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.52);
+ animation: fadeIn 0.22s forwards;
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_footer.scss b/packages/auditor-backoffice-ui/src/scss/_footer.scss
new file mode 100644
index 000000000..5855af742
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_footer.scss
@@ -0,0 +1,35 @@
+/*
+ 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)
+ */
+
+footer.footer {
+ .logo {
+ img {
+ width: auto;
+ height: $footer-logo-height;
+ }
+ }
+}
+
+@include mobile {
+ .footer-copyright {
+ text-align: center;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_form.scss b/packages/auditor-backoffice-ui/src/scss/_form.scss
new file mode 100644
index 000000000..bd28a17cf
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_form.scss
@@ -0,0 +1,71 @@
+/*
+ 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)
+ */
+
+.field {
+ &.has-check {
+ .field-body {
+ margin-top: $default-padding * 0.125;
+ }
+ }
+ .control {
+ .mdi-24px.mdi-set,
+ .mdi-24px.mdi:before {
+ font-size: inherit;
+ }
+ }
+}
+.upload {
+ .upload-draggable {
+ display: block;
+ }
+}
+
+.input,
+.textarea,
+select {
+ box-shadow: none;
+
+ &:focus,
+ &:active {
+ box-shadow: none !important;
+ }
+}
+
+.switch input[type="checkbox"] + .check:before {
+ box-shadow: none;
+}
+
+.switch,
+.b-checkbox.checkbox {
+ input[type="checkbox"] {
+ &:focus + .check,
+ &:focus:checked + .check {
+ box-shadow: none !important;
+ }
+ }
+}
+
+.b-checkbox.checkbox input[type="checkbox"],
+.b-radio.radio input[type="radio"] {
+ & + .check {
+ border: $checkbox-border;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
new file mode 100644
index 000000000..0276468d7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
@@ -0,0 +1,55 @@
+/*
+ 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)
+ */
+
+section.hero.is-hero-bar {
+ background-color: $hero-bar-background;
+ border-bottom: $light-border;
+
+ .hero-body {
+ padding: $default-padding;
+
+ .level-item {
+ &.is-hero-avatar-item {
+ margin-right: $default-padding;
+ }
+
+ > div > .level {
+ margin-bottom: $default-padding * 0.5;
+ }
+
+ .subtitle + p {
+ margin-top: $default-padding * 0.5;
+ }
+ }
+
+ .button {
+ &.is-hero-button {
+ background-color: rgba($white, 0.5);
+ font-weight: 300;
+ @include transition(background-color);
+
+ &:hover {
+ background-color: $white;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_loading.scss b/packages/auditor-backoffice-ui/src/scss/_loading.scss
new file mode 100644
index 000000000..d88d8c355
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_loading.scss
@@ -0,0 +1,51 @@
+/*
+ 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/>
+ */
+
+.lds-ring {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+.lds-ring div {
+ box-sizing: border-box;
+ display: block;
+ position: absolute;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border: 8px solid black;
+ border-radius: 50%;
+ animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+ border-color: black transparent transparent transparent;
+}
+.lds-ring div:nth-child(1) {
+ animation-delay: -0.45s;
+}
+.lds-ring div:nth-child(2) {
+ animation-delay: -0.3s;
+}
+.lds-ring div:nth-child(3) {
+ animation-delay: -0.15s;
+}
+@keyframes lds-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_main-section.scss b/packages/auditor-backoffice-ui/src/scss/_main-section.scss
new file mode 100644
index 000000000..5a8b20ba0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_main-section.scss
@@ -0,0 +1,24 @@
+/*
+ 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)
+ */
+
+section.section.is-main-section {
+ padding-top: $default-padding;
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_misc.scss b/packages/auditor-backoffice-ui/src/scss/_misc.scss
new file mode 100644
index 000000000..045d087e2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_misc.scss
@@ -0,0 +1,50 @@
+/*
+ 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)
+ */
+
+.is-user-avatar {
+ &.has-max-width {
+ max-width: $size-base * 7;
+ }
+
+ &.is-aligned-center {
+ margin: 0 auto;
+ }
+
+ img {
+ margin: 0 auto;
+ border-radius: $radius-rounded;
+ }
+}
+
+.icon.has-update-mark {
+ position: relative;
+
+ &:after {
+ content: "";
+ width: $icon-update-mark-size;
+ height: $icon-update-mark-size;
+ position: absolute;
+ top: 1px;
+ right: 1px;
+ background-color: $icon-update-mark-color;
+ border-radius: $radius-rounded;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_mixins.scss b/packages/auditor-backoffice-ui/src/scss/_mixins.scss
new file mode 100644
index 000000000..f119ec68a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_mixins.scss
@@ -0,0 +1,34 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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)
+ */
+
+@mixin transition($t) {
+ transition: $t 250ms ease-in-out 50ms;
+}
+
+@mixin icon-with-update-mark($icon-base-width) {
+ .icon {
+ width: $icon-base-width;
+
+ &.has-update-mark:after {
+ right: calc($icon-base-width / 2) - 0.85;
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_modal.scss b/packages/auditor-backoffice-ui/src/scss/_modal.scss
new file mode 100644
index 000000000..b2bfd3e9e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_modal.scss
@@ -0,0 +1,35 @@
+/*
+ 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)
+ */
+
+.modal-card {
+ width: $modal-card-width;
+}
+
+.modal-card-foot {
+ background-color: $modal-card-foot-background-color;
+}
+
+@include mobile {
+ .modal .animation-content .modal-card {
+ width: $modal-card-width-mobile;
+ margin: 0 auto;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
new file mode 100644
index 000000000..406e0392f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
@@ -0,0 +1,144 @@
+/*
+ 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)
+ */
+
+nav.navbar {
+ box-shadow: $navbar-box-shadow;
+
+ .navbar-item {
+ &.has-user-avatar {
+ .is-user-avatar {
+ margin-right: $default-padding * 0.5;
+ display: inline-flex;
+ width: $navbar-avatar-size;
+ height: $navbar-avatar-size;
+ }
+ }
+
+ &.has-divider {
+ border-right: $navbar-divider-border;
+ }
+
+ &.no-left-space {
+ padding-left: 0;
+ }
+
+ &.has-dropdown {
+ padding-right: 0;
+ padding-left: 0;
+
+ .navbar-link {
+ padding-right: $navbar-item-h-padding;
+ padding-left: $navbar-item-h-padding;
+ }
+ }
+
+ &.has-control {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+
+ .control {
+ .input {
+ color: $navbar-input-color;
+ border: 0;
+ box-shadow: none;
+ background: transparent;
+
+ &::placeholder {
+ color: $navbar-input-placeholder-color;
+ }
+ }
+ }
+ }
+}
+
+@include touch {
+ nav.navbar {
+ display: flex;
+ padding-right: 0;
+
+ .navbar-brand {
+ flex: 1;
+
+ &.is-right {
+ flex: none;
+ }
+ }
+
+ .navbar-item {
+ &.no-left-space-touch {
+ padding-left: 0;
+ }
+ }
+
+ .navbar-menu {
+ position: absolute;
+ width: 100vw;
+ padding-top: 0;
+ top: $navbar-height;
+ left: 0;
+
+ .navbar-item {
+ .icon:first-child {
+ margin-right: $default-padding * 0.5;
+ }
+
+ &.has-dropdown {
+ > .navbar-link {
+ background-color: $white-ter;
+ .icon:last-child {
+ display: none;
+ }
+ }
+ }
+
+ &.has-user-avatar {
+ > .navbar-link {
+ display: flex;
+ align-items: center;
+ padding-top: $default-padding * 0.5;
+ padding-bottom: $default-padding * 0.5;
+ }
+ }
+ }
+ }
+ }
+}
+
+@include desktop {
+ nav.navbar {
+ .navbar-item {
+ padding-right: $navbar-item-h-padding;
+ padding-left: $navbar-item-h-padding;
+
+ &:not(.is-desktop-icon-only) {
+ .icon:first-child {
+ margin-right: $default-padding * 0.5;
+ }
+ }
+ &.is-desktop-icon-only {
+ span:not(.icon) {
+ display: none;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_table.scss b/packages/auditor-backoffice-ui/src/scss/_table.scss
new file mode 100644
index 000000000..e4fbfc7b3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_table.scss
@@ -0,0 +1,179 @@
+/*
+ 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)
+ */
+
+table.table {
+ thead {
+ th {
+ border-bottom-width: 1px;
+ }
+ }
+
+ td,
+ th {
+ &.checkbox-cell {
+ .b-checkbox.checkbox:not(.button) {
+ margin-right: 0;
+ width: 20px;
+
+ .control-label {
+ display: none;
+ padding: 0;
+ }
+ }
+ }
+ }
+
+ td {
+ .image {
+ margin: 0 auto;
+ width: $table-avatar-size;
+ height: $table-avatar-size;
+ }
+
+ &.is-progress-col {
+ min-width: 5rem;
+ vertical-align: middle;
+ }
+ }
+}
+
+.b-table {
+ .table {
+ border: 0;
+ border-radius: 0;
+ }
+
+ /* This stylizes buefy's pagination */
+ .table-wrapper {
+ margin-bottom: 0;
+ }
+
+ .table-wrapper + .level {
+ padding: $notification-padding;
+ padding-left: $card-content-padding;
+ padding-right: $card-content-padding;
+ margin: 0;
+ border-top: $base-color-light;
+ background: $notification-background-color;
+
+ .pagination-link {
+ background: $button-background-color;
+ color: $button-color;
+ border-color: $button-border-color;
+
+ &.is-current {
+ border-color: $button-active-border-color;
+ }
+ }
+
+ .pagination-previous,
+ .pagination-next,
+ .pagination-link {
+ border-color: $button-border-color;
+ color: $base-color;
+
+ &[disabled] {
+ background-color: transparent;
+ }
+ }
+ }
+}
+
+@include mobile {
+ .card {
+ &.has-table {
+ .b-table {
+ .table-wrapper + .level {
+ .level-left + .level-right {
+ margin-top: 0;
+ }
+ }
+ }
+ }
+ &.has-mobile-sort-spaced {
+ .b-table {
+ .field.table-mobile-sort {
+ padding-top: $default-padding * 0.5;
+ }
+ }
+ }
+ }
+ .b-table {
+ .field.table-mobile-sort {
+ padding: 0 $default-padding * 0.5;
+ }
+
+ .table-wrapper.has-mobile-cards {
+ tr {
+ box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
+ margin-bottom: 3px !important;
+ }
+ td {
+ &.is-progress-col {
+ span,
+ progress {
+ display: flex;
+ width: 45%;
+ align-items: center;
+ align-self: center;
+ }
+ }
+
+ &.checkbox-cell,
+ &.is-image-cell {
+ border-bottom: 0 !important;
+ }
+
+ &.checkbox-cell,
+ &.is-actions-cell {
+ &:before {
+ display: none;
+ }
+ }
+
+ &.has-no-head-mobile {
+ &:before {
+ display: none;
+ }
+
+ span {
+ display: block;
+ width: 100%;
+ }
+
+ &.is-progress-col {
+ progress {
+ width: 100%;
+ }
+ }
+
+ &.is-image-cell {
+ .image {
+ width: $table-avatar-size-mobile;
+ height: auto;
+ margin: 0 auto $default-padding * 0.25;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_theme-default.scss b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
new file mode 100644
index 000000000..e74ece0e9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
@@ -0,0 +1,136 @@
+/*
+ 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)
+ */
+
+/* We'll need some initial vars to use here */
+@import "node_modules/bulma/sass/utilities/initial-variables";
+
+/* Base: Size */
+$size-base: 1rem;
+$default-padding: $size-base * 1.5;
+
+/* Default font */
+$family-sans-serif: "Nunito", sans-serif;
+
+/* Base color */
+$base-color: #2e323a;
+$base-color-light: rgba(24, 28, 33, 0.06);
+
+/* General overrides */
+$primary: $turquoise;
+$body-background-color: #f8f8f8;
+$link: $blue;
+$link-visited: $purple;
+$light-border: 1px solid $base-color-light;
+$hr-height: 1px;
+
+/* NavBar: specifics */
+$navbar-input-color: $grey-darker;
+$navbar-input-placeholder-color: $grey-lighter;
+$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04);
+$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25);
+$navbar-item-h-padding: $default-padding * 0.75;
+$navbar-avatar-size: 1.75rem;
+
+/* Aside: Bulma override */
+$menu-item-radius: 0;
+$menu-list-link-padding: $size-base * 0.5 0;
+$menu-label-color: lighten($base-color, 25%);
+$menu-item-color: lighten($base-color, 30%);
+$menu-item-hover-color: $white;
+$menu-item-hover-background-color: darken($base-color, 3.5%);
+$menu-item-active-color: $white;
+$menu-item-active-background-color: darken($base-color, 2.5%);
+
+/* Aside: specifics */
+$aside-width: $size-base * 14;
+$aside-mobile-width: $size-base * 15;
+$aside-icon-width: $size-base * 3;
+$aside-submenu-font-size: $size-base * 0.95;
+$aside-box-shadow: none;
+$aside-background-color: $base-color;
+$aside-tools-background-color: darken($aside-background-color, 10%);
+$aside-tools-color: $white;
+
+/* Title Bar: specifics */
+$title-bar-color: $grey;
+$title-bar-active-color: $black-ter;
+
+/* Hero Bar: specifics */
+$hero-bar-background: $white;
+
+/* Card: Bulma override */
+$card-shadow: none;
+$card-header-shadow: none;
+
+/* Card: specifics */
+$card-border: 1px solid $base-color-light;
+$card-header-border-bottom-color: $base-color-light;
+
+/* Table: Bulma override */
+$table-cell-border: 1px solid $white-bis;
+
+/* Table: specifics */
+$table-avatar-size: $size-base * 1.5;
+$table-avatar-size-mobile: 25vw;
+
+/* Form */
+$checkbox-border: 1px solid $base-color;
+
+/* Modal card: Bulma override */
+$modal-card-head-background-color: $white-ter;
+$modal-card-title-size: $size-base;
+$modal-card-body-padding: $default-padding 20px;
+$modal-card-head-border-bottom: 1px solid $white-ter;
+$modal-card-foot-border-top: 0;
+
+/* Modal card: specifics */
+$modal-card-width: 80vw;
+$modal-card-width-mobile: 90vw;
+$modal-card-foot-background-color: $white-ter;
+
+/* Notification: Bulma override */
+$notification-padding: $default-padding * 0.75 $default-padding;
+
+/* Footer: Bulma override */
+$footer-background-color: $white;
+$footer-padding: $default-padding * 0.33 $default-padding;
+
+/* Footer: specifics */
+$footer-logo-height: $size-base * 2;
+
+/* Progress: Bulma override */
+$progress-bar-background-color: $grey-lighter;
+
+/* Icon: specifics */
+$icon-update-mark-size: $size-base * 0.5;
+$icon-update-mark-color: $yellow;
+
+$input-disabled-border-color: $grey-lighter;
+$table-row-hover-background-color: hsl(0, 0%, 80%);
+
+.menu-list {
+ div {
+ border-radius: $menu-item-radius;
+ color: $menu-item-color;
+ display: block;
+ padding: $menu-list-link-padding;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_tiles.scss b/packages/auditor-backoffice-ui/src/scss/_tiles.scss
new file mode 100644
index 000000000..94dd6c21d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_tiles.scss
@@ -0,0 +1,24 @@
+/*
+ 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)
+ */
+
+.is-tiles-wrapper {
+ margin-bottom: $default-padding;
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/_title-bar.scss b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
new file mode 100644
index 000000000..bac3f6b42
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
@@ -0,0 +1,50 @@
+/*
+ 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)
+ */
+
+section.section.is-title-bar {
+ padding: $default-padding;
+ border-bottom: $light-border;
+
+ ul {
+ li {
+ display: inline-block;
+ padding: 0 $default-padding * 0.5 0 0;
+ font-size: $default-padding;
+ color: $title-bar-color;
+
+ &:after {
+ display: inline-block;
+ content: "/";
+ padding-left: $default-padding * 0.5;
+ }
+
+ &:last-child {
+ padding-right: 0;
+ font-weight: 900;
+ color: $title-bar-active-color;
+
+ &:after {
+ display: none;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
new file mode 100644
index 000000000..7665ee336
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
new file mode 100644
index 000000000..a578506e8
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
@@ -0,0 +1,22 @@
+/*
+ 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/>
+ */
+
+@font-face {
+ font-family: "Nunito";
+ font-style: normal;
+ font-weight: 400;
+ src: url(./XRXV3I6Li01BKofINeaE.ttf) format("truetype");
+}
diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
new file mode 100644
index 000000000..ab6b25ded
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
new file mode 100644
index 000000000..824be10fa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
new file mode 100644
index 000000000..7e087c1de
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
new file mode 100644
index 000000000..b5caa4ddc
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
Binary files differ
diff --git a/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
new file mode 100644
index 000000000..2b8a2b244
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
@@ -0,0 +1,15109 @@
+@font-face {
+ font-family: "Material Design Icons";
+ src: url("./fonts/materialdesignicons-webfont-4.9.95.eot");
+ src: url("./fonts/materialdesignicons-webfont-4.9.95.woff2") format("woff2"),
+ url("./fonts/materialdesignicons-webfont-4.9.95.woff") format("woff"),
+ url("./fonts/materialdesignicons-webfont-4.9.95.ttf") format("truetype");
+ font-weight: normal;
+ font-style: normal;
+}
+.mdi:before,
+.mdi-set {
+ display: inline-block;
+ font: normal normal normal 24px/1 "Material Design Icons";
+ font-size: inherit;
+ text-rendering: auto;
+ line-height: inherit;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.mdi-ab-testing::before {
+ content: "\F001C";
+}
+.mdi-abjad-arabic::before {
+ content: "\F0353";
+}
+.mdi-abjad-hebrew::before {
+ content: "\F0354";
+}
+.mdi-abugida-devanagari::before {
+ content: "\F0355";
+}
+.mdi-abugida-thai::before {
+ content: "\F0356";
+}
+.mdi-access-point::before {
+ content: "\F002";
+}
+.mdi-access-point-network::before {
+ content: "\F003";
+}
+.mdi-access-point-network-off::before {
+ content: "\FBBD";
+}
+.mdi-account::before {
+ content: "\F004";
+}
+.mdi-account-alert::before {
+ content: "\F005";
+}
+.mdi-account-alert-outline::before {
+ content: "\FB2C";
+}
+.mdi-account-arrow-left::before {
+ content: "\FB2D";
+}
+.mdi-account-arrow-left-outline::before {
+ content: "\FB2E";
+}
+.mdi-account-arrow-right::before {
+ content: "\FB2F";
+}
+.mdi-account-arrow-right-outline::before {
+ content: "\FB30";
+}
+.mdi-account-badge::before {
+ content: "\FD83";
+}
+.mdi-account-badge-alert::before {
+ content: "\FD84";
+}
+.mdi-account-badge-alert-outline::before {
+ content: "\FD85";
+}
+.mdi-account-badge-horizontal::before {
+ content: "\FDF0";
+}
+.mdi-account-badge-horizontal-outline::before {
+ content: "\FDF1";
+}
+.mdi-account-badge-outline::before {
+ content: "\FD86";
+}
+.mdi-account-box::before {
+ content: "\F006";
+}
+.mdi-account-box-multiple::before {
+ content: "\F933";
+}
+.mdi-account-box-multiple-outline::before {
+ content: "\F002C";
+}
+.mdi-account-box-outline::before {
+ content: "\F007";
+}
+.mdi-account-cancel::before {
+ content: "\F030A";
+}
+.mdi-account-cancel-outline::before {
+ content: "\F030B";
+}
+.mdi-account-card-details::before {
+ content: "\F5D2";
+}
+.mdi-account-card-details-outline::before {
+ content: "\FD87";
+}
+.mdi-account-cash::before {
+ content: "\F00C2";
+}
+.mdi-account-cash-outline::before {
+ content: "\F00C3";
+}
+.mdi-account-check::before {
+ content: "\F008";
+}
+.mdi-account-check-outline::before {
+ content: "\FBBE";
+}
+.mdi-account-child::before {
+ content: "\FA88";
+}
+.mdi-account-child-circle::before {
+ content: "\FA89";
+}
+.mdi-account-child-outline::before {
+ content: "\F00F3";
+}
+.mdi-account-circle::before {
+ content: "\F009";
+}
+.mdi-account-circle-outline::before {
+ content: "\FB31";
+}
+.mdi-account-clock::before {
+ content: "\FB32";
+}
+.mdi-account-clock-outline::before {
+ content: "\FB33";
+}
+.mdi-account-cog::before {
+ content: "\F039B";
+}
+.mdi-account-cog-outline::before {
+ content: "\F039C";
+}
+.mdi-account-convert::before {
+ content: "\F00A";
+}
+.mdi-account-convert-outline::before {
+ content: "\F032C";
+}
+.mdi-account-details::before {
+ content: "\F631";
+}
+.mdi-account-details-outline::before {
+ content: "\F039D";
+}
+.mdi-account-edit::before {
+ content: "\F6BB";
+}
+.mdi-account-edit-outline::before {
+ content: "\F001D";
+}
+.mdi-account-group::before {
+ content: "\F848";
+}
+.mdi-account-group-outline::before {
+ content: "\FB34";
+}
+.mdi-account-heart::before {
+ content: "\F898";
+}
+.mdi-account-heart-outline::before {
+ content: "\FBBF";
+}
+.mdi-account-key::before {
+ content: "\F00B";
+}
+.mdi-account-key-outline::before {
+ content: "\FBC0";
+}
+.mdi-account-lock::before {
+ content: "\F0189";
+}
+.mdi-account-lock-outline::before {
+ content: "\F018A";
+}
+.mdi-account-minus::before {
+ content: "\F00D";
+}
+.mdi-account-minus-outline::before {
+ content: "\FAEB";
+}
+.mdi-account-multiple::before {
+ content: "\F00E";
+}
+.mdi-account-multiple-check::before {
+ content: "\F8C4";
+}
+.mdi-account-multiple-check-outline::before {
+ content: "\F0229";
+}
+.mdi-account-multiple-minus::before {
+ content: "\F5D3";
+}
+.mdi-account-multiple-minus-outline::before {
+ content: "\FBC1";
+}
+.mdi-account-multiple-outline::before {
+ content: "\F00F";
+}
+.mdi-account-multiple-plus::before {
+ content: "\F010";
+}
+.mdi-account-multiple-plus-outline::before {
+ content: "\F7FF";
+}
+.mdi-account-multiple-remove::before {
+ content: "\F0235";
+}
+.mdi-account-multiple-remove-outline::before {
+ content: "\F0236";
+}
+.mdi-account-network::before {
+ content: "\F011";
+}
+.mdi-account-network-outline::before {
+ content: "\FBC2";
+}
+.mdi-account-off::before {
+ content: "\F012";
+}
+.mdi-account-off-outline::before {
+ content: "\FBC3";
+}
+.mdi-account-outline::before {
+ content: "\F013";
+}
+.mdi-account-plus::before {
+ content: "\F014";
+}
+.mdi-account-plus-outline::before {
+ content: "\F800";
+}
+.mdi-account-question::before {
+ content: "\FB35";
+}
+.mdi-account-question-outline::before {
+ content: "\FB36";
+}
+.mdi-account-remove::before {
+ content: "\F015";
+}
+.mdi-account-remove-outline::before {
+ content: "\FAEC";
+}
+.mdi-account-search::before {
+ content: "\F016";
+}
+.mdi-account-search-outline::before {
+ content: "\F934";
+}
+.mdi-account-settings::before {
+ content: "\F630";
+}
+.mdi-account-settings-outline::before {
+ content: "\F00F4";
+}
+.mdi-account-star::before {
+ content: "\F017";
+}
+.mdi-account-star-outline::before {
+ content: "\FBC4";
+}
+.mdi-account-supervisor::before {
+ content: "\FA8A";
+}
+.mdi-account-supervisor-circle::before {
+ content: "\FA8B";
+}
+.mdi-account-supervisor-outline::before {
+ content: "\F0158";
+}
+.mdi-account-switch::before {
+ content: "\F019";
+}
+.mdi-account-tie::before {
+ content: "\FCBF";
+}
+.mdi-account-tie-outline::before {
+ content: "\F00F5";
+}
+.mdi-account-tie-voice::before {
+ content: "\F0333";
+}
+.mdi-account-tie-voice-off::before {
+ content: "\F0335";
+}
+.mdi-account-tie-voice-off-outline::before {
+ content: "\F0336";
+}
+.mdi-account-tie-voice-outline::before {
+ content: "\F0334";
+}
+.mdi-accusoft::before {
+ content: "\F849";
+}
+.mdi-adjust::before {
+ content: "\F01A";
+}
+.mdi-adobe::before {
+ content: "\F935";
+}
+.mdi-adobe-acrobat::before {
+ content: "\FFBD";
+}
+.mdi-air-conditioner::before {
+ content: "\F01B";
+}
+.mdi-air-filter::before {
+ content: "\FD1F";
+}
+.mdi-air-horn::before {
+ content: "\FD88";
+}
+.mdi-air-humidifier::before {
+ content: "\F00C4";
+}
+.mdi-air-purifier::before {
+ content: "\FD20";
+}
+.mdi-airbag::before {
+ content: "\FBC5";
+}
+.mdi-airballoon::before {
+ content: "\F01C";
+}
+.mdi-airballoon-outline::before {
+ content: "\F002D";
+}
+.mdi-airplane::before {
+ content: "\F01D";
+}
+.mdi-airplane-landing::before {
+ content: "\F5D4";
+}
+.mdi-airplane-off::before {
+ content: "\F01E";
+}
+.mdi-airplane-takeoff::before {
+ content: "\F5D5";
+}
+.mdi-airplay::before {
+ content: "\F01F";
+}
+.mdi-airport::before {
+ content: "\F84A";
+}
+.mdi-alarm::before {
+ content: "\F020";
+}
+.mdi-alarm-bell::before {
+ content: "\F78D";
+}
+.mdi-alarm-check::before {
+ content: "\F021";
+}
+.mdi-alarm-light::before {
+ content: "\F78E";
+}
+.mdi-alarm-light-outline::before {
+ content: "\FBC6";
+}
+.mdi-alarm-multiple::before {
+ content: "\F022";
+}
+.mdi-alarm-note::before {
+ content: "\FE8E";
+}
+.mdi-alarm-note-off::before {
+ content: "\FE8F";
+}
+.mdi-alarm-off::before {
+ content: "\F023";
+}
+.mdi-alarm-plus::before {
+ content: "\F024";
+}
+.mdi-alarm-snooze::before {
+ content: "\F68D";
+}
+.mdi-album::before {
+ content: "\F025";
+}
+.mdi-alert::before {
+ content: "\F026";
+}
+.mdi-alert-box::before {
+ content: "\F027";
+}
+.mdi-alert-box-outline::before {
+ content: "\FCC0";
+}
+.mdi-alert-circle::before {
+ content: "\F028";
+}
+.mdi-alert-circle-check::before {
+ content: "\F0218";
+}
+.mdi-alert-circle-check-outline::before {
+ content: "\F0219";
+}
+.mdi-alert-circle-outline::before {
+ content: "\F5D6";
+}
+.mdi-alert-decagram::before {
+ content: "\F6BC";
+}
+.mdi-alert-decagram-outline::before {
+ content: "\FCC1";
+}
+.mdi-alert-octagon::before {
+ content: "\F029";
+}
+.mdi-alert-octagon-outline::before {
+ content: "\FCC2";
+}
+.mdi-alert-octagram::before {
+ content: "\F766";
+}
+.mdi-alert-octagram-outline::before {
+ content: "\FCC3";
+}
+.mdi-alert-outline::before {
+ content: "\F02A";
+}
+.mdi-alert-rhombus::before {
+ content: "\F01F9";
+}
+.mdi-alert-rhombus-outline::before {
+ content: "\F01FA";
+}
+.mdi-alien::before {
+ content: "\F899";
+}
+.mdi-alien-outline::before {
+ content: "\F00F6";
+}
+.mdi-align-horizontal-center::before {
+ content: "\F01EE";
+}
+.mdi-align-horizontal-left::before {
+ content: "\F01ED";
+}
+.mdi-align-horizontal-right::before {
+ content: "\F01EF";
+}
+.mdi-align-vertical-bottom::before {
+ content: "\F01F0";
+}
+.mdi-align-vertical-center::before {
+ content: "\F01F1";
+}
+.mdi-align-vertical-top::before {
+ content: "\F01F2";
+}
+.mdi-all-inclusive::before {
+ content: "\F6BD";
+}
+.mdi-allergy::before {
+ content: "\F0283";
+}
+.mdi-alpha::before {
+ content: "\F02B";
+}
+.mdi-alpha-a::before {
+ content: "\41";
+}
+.mdi-alpha-a-box::before {
+ content: "\FAED";
+}
+.mdi-alpha-a-box-outline::before {
+ content: "\FBC7";
+}
+.mdi-alpha-a-circle::before {
+ content: "\FBC8";
+}
+.mdi-alpha-a-circle-outline::before {
+ content: "\FBC9";
+}
+.mdi-alpha-b::before {
+ content: "\42";
+}
+.mdi-alpha-b-box::before {
+ content: "\FAEE";
+}
+.mdi-alpha-b-box-outline::before {
+ content: "\FBCA";
+}
+.mdi-alpha-b-circle::before {
+ content: "\FBCB";
+}
+.mdi-alpha-b-circle-outline::before {
+ content: "\FBCC";
+}
+.mdi-alpha-c::before {
+ content: "\43";
+}
+.mdi-alpha-c-box::before {
+ content: "\FAEF";
+}
+.mdi-alpha-c-box-outline::before {
+ content: "\FBCD";
+}
+.mdi-alpha-c-circle::before {
+ content: "\FBCE";
+}
+.mdi-alpha-c-circle-outline::before {
+ content: "\FBCF";
+}
+.mdi-alpha-d::before {
+ content: "\44";
+}
+.mdi-alpha-d-box::before {
+ content: "\FAF0";
+}
+.mdi-alpha-d-box-outline::before {
+ content: "\FBD0";
+}
+.mdi-alpha-d-circle::before {
+ content: "\FBD1";
+}
+.mdi-alpha-d-circle-outline::before {
+ content: "\FBD2";
+}
+.mdi-alpha-e::before {
+ content: "\45";
+}
+.mdi-alpha-e-box::before {
+ content: "\FAF1";
+}
+.mdi-alpha-e-box-outline::before {
+ content: "\FBD3";
+}
+.mdi-alpha-e-circle::before {
+ content: "\FBD4";
+}
+.mdi-alpha-e-circle-outline::before {
+ content: "\FBD5";
+}
+.mdi-alpha-f::before {
+ content: "\46";
+}
+.mdi-alpha-f-box::before {
+ content: "\FAF2";
+}
+.mdi-alpha-f-box-outline::before {
+ content: "\FBD6";
+}
+.mdi-alpha-f-circle::before {
+ content: "\FBD7";
+}
+.mdi-alpha-f-circle-outline::before {
+ content: "\FBD8";
+}
+.mdi-alpha-g::before {
+ content: "\47";
+}
+.mdi-alpha-g-box::before {
+ content: "\FAF3";
+}
+.mdi-alpha-g-box-outline::before {
+ content: "\FBD9";
+}
+.mdi-alpha-g-circle::before {
+ content: "\FBDA";
+}
+.mdi-alpha-g-circle-outline::before {
+ content: "\FBDB";
+}
+.mdi-alpha-h::before {
+ content: "\48";
+}
+.mdi-alpha-h-box::before {
+ content: "\FAF4";
+}
+.mdi-alpha-h-box-outline::before {
+ content: "\FBDC";
+}
+.mdi-alpha-h-circle::before {
+ content: "\FBDD";
+}
+.mdi-alpha-h-circle-outline::before {
+ content: "\FBDE";
+}
+.mdi-alpha-i::before {
+ content: "\49";
+}
+.mdi-alpha-i-box::before {
+ content: "\FAF5";
+}
+.mdi-alpha-i-box-outline::before {
+ content: "\FBDF";
+}
+.mdi-alpha-i-circle::before {
+ content: "\FBE0";
+}
+.mdi-alpha-i-circle-outline::before {
+ content: "\FBE1";
+}
+.mdi-alpha-j::before {
+ content: "\4A";
+}
+.mdi-alpha-j-box::before {
+ content: "\FAF6";
+}
+.mdi-alpha-j-box-outline::before {
+ content: "\FBE2";
+}
+.mdi-alpha-j-circle::before {
+ content: "\FBE3";
+}
+.mdi-alpha-j-circle-outline::before {
+ content: "\FBE4";
+}
+.mdi-alpha-k::before {
+ content: "\4B";
+}
+.mdi-alpha-k-box::before {
+ content: "\FAF7";
+}
+.mdi-alpha-k-box-outline::before {
+ content: "\FBE5";
+}
+.mdi-alpha-k-circle::before {
+ content: "\FBE6";
+}
+.mdi-alpha-k-circle-outline::before {
+ content: "\FBE7";
+}
+.mdi-alpha-l::before {
+ content: "\4C";
+}
+.mdi-alpha-l-box::before {
+ content: "\FAF8";
+}
+.mdi-alpha-l-box-outline::before {
+ content: "\FBE8";
+}
+.mdi-alpha-l-circle::before {
+ content: "\FBE9";
+}
+.mdi-alpha-l-circle-outline::before {
+ content: "\FBEA";
+}
+.mdi-alpha-m::before {
+ content: "\4D";
+}
+.mdi-alpha-m-box::before {
+ content: "\FAF9";
+}
+.mdi-alpha-m-box-outline::before {
+ content: "\FBEB";
+}
+.mdi-alpha-m-circle::before {
+ content: "\FBEC";
+}
+.mdi-alpha-m-circle-outline::before {
+ content: "\FBED";
+}
+.mdi-alpha-n::before {
+ content: "\4E";
+}
+.mdi-alpha-n-box::before {
+ content: "\FAFA";
+}
+.mdi-alpha-n-box-outline::before {
+ content: "\FBEE";
+}
+.mdi-alpha-n-circle::before {
+ content: "\FBEF";
+}
+.mdi-alpha-n-circle-outline::before {
+ content: "\FBF0";
+}
+.mdi-alpha-o::before {
+ content: "\4F";
+}
+.mdi-alpha-o-box::before {
+ content: "\FAFB";
+}
+.mdi-alpha-o-box-outline::before {
+ content: "\FBF1";
+}
+.mdi-alpha-o-circle::before {
+ content: "\FBF2";
+}
+.mdi-alpha-o-circle-outline::before {
+ content: "\FBF3";
+}
+.mdi-alpha-p::before {
+ content: "\50";
+}
+.mdi-alpha-p-box::before {
+ content: "\FAFC";
+}
+.mdi-alpha-p-box-outline::before {
+ content: "\FBF4";
+}
+.mdi-alpha-p-circle::before {
+ content: "\FBF5";
+}
+.mdi-alpha-p-circle-outline::before {
+ content: "\FBF6";
+}
+.mdi-alpha-q::before {
+ content: "\51";
+}
+.mdi-alpha-q-box::before {
+ content: "\FAFD";
+}
+.mdi-alpha-q-box-outline::before {
+ content: "\FBF7";
+}
+.mdi-alpha-q-circle::before {
+ content: "\FBF8";
+}
+.mdi-alpha-q-circle-outline::before {
+ content: "\FBF9";
+}
+.mdi-alpha-r::before {
+ content: "\52";
+}
+.mdi-alpha-r-box::before {
+ content: "\FAFE";
+}
+.mdi-alpha-r-box-outline::before {
+ content: "\FBFA";
+}
+.mdi-alpha-r-circle::before {
+ content: "\FBFB";
+}
+.mdi-alpha-r-circle-outline::before {
+ content: "\FBFC";
+}
+.mdi-alpha-s::before {
+ content: "\53";
+}
+.mdi-alpha-s-box::before {
+ content: "\FAFF";
+}
+.mdi-alpha-s-box-outline::before {
+ content: "\FBFD";
+}
+.mdi-alpha-s-circle::before {
+ content: "\FBFE";
+}
+.mdi-alpha-s-circle-outline::before {
+ content: "\FBFF";
+}
+.mdi-alpha-t::before {
+ content: "\54";
+}
+.mdi-alpha-t-box::before {
+ content: "\FB00";
+}
+.mdi-alpha-t-box-outline::before {
+ content: "\FC00";
+}
+.mdi-alpha-t-circle::before {
+ content: "\FC01";
+}
+.mdi-alpha-t-circle-outline::before {
+ content: "\FC02";
+}
+.mdi-alpha-u::before {
+ content: "\55";
+}
+.mdi-alpha-u-box::before {
+ content: "\FB01";
+}
+.mdi-alpha-u-box-outline::before {
+ content: "\FC03";
+}
+.mdi-alpha-u-circle::before {
+ content: "\FC04";
+}
+.mdi-alpha-u-circle-outline::before {
+ content: "\FC05";
+}
+.mdi-alpha-v::before {
+ content: "\56";
+}
+.mdi-alpha-v-box::before {
+ content: "\FB02";
+}
+.mdi-alpha-v-box-outline::before {
+ content: "\FC06";
+}
+.mdi-alpha-v-circle::before {
+ content: "\FC07";
+}
+.mdi-alpha-v-circle-outline::before {
+ content: "\FC08";
+}
+.mdi-alpha-w::before {
+ content: "\57";
+}
+.mdi-alpha-w-box::before {
+ content: "\FB03";
+}
+.mdi-alpha-w-box-outline::before {
+ content: "\FC09";
+}
+.mdi-alpha-w-circle::before {
+ content: "\FC0A";
+}
+.mdi-alpha-w-circle-outline::before {
+ content: "\FC0B";
+}
+.mdi-alpha-x::before {
+ content: "\58";
+}
+.mdi-alpha-x-box::before {
+ content: "\FB04";
+}
+.mdi-alpha-x-box-outline::before {
+ content: "\FC0C";
+}
+.mdi-alpha-x-circle::before {
+ content: "\FC0D";
+}
+.mdi-alpha-x-circle-outline::before {
+ content: "\FC0E";
+}
+.mdi-alpha-y::before {
+ content: "\59";
+}
+.mdi-alpha-y-box::before {
+ content: "\FB05";
+}
+.mdi-alpha-y-box-outline::before {
+ content: "\FC0F";
+}
+.mdi-alpha-y-circle::before {
+ content: "\FC10";
+}
+.mdi-alpha-y-circle-outline::before {
+ content: "\FC11";
+}
+.mdi-alpha-z::before {
+ content: "\5A";
+}
+.mdi-alpha-z-box::before {
+ content: "\FB06";
+}
+.mdi-alpha-z-box-outline::before {
+ content: "\FC12";
+}
+.mdi-alpha-z-circle::before {
+ content: "\FC13";
+}
+.mdi-alpha-z-circle-outline::before {
+ content: "\FC14";
+}
+.mdi-alphabet-aurebesh::before {
+ content: "\F0357";
+}
+.mdi-alphabet-cyrillic::before {
+ content: "\F0358";
+}
+.mdi-alphabet-greek::before {
+ content: "\F0359";
+}
+.mdi-alphabet-latin::before {
+ content: "\F035A";
+}
+.mdi-alphabet-piqad::before {
+ content: "\F035B";
+}
+.mdi-alphabet-tengwar::before {
+ content: "\F0362";
+}
+.mdi-alphabetical::before {
+ content: "\F02C";
+}
+.mdi-alphabetical-off::before {
+ content: "\F002E";
+}
+.mdi-alphabetical-variant::before {
+ content: "\F002F";
+}
+.mdi-alphabetical-variant-off::before {
+ content: "\F0030";
+}
+.mdi-altimeter::before {
+ content: "\F5D7";
+}
+.mdi-amazon::before {
+ content: "\F02D";
+}
+.mdi-amazon-alexa::before {
+ content: "\F8C5";
+}
+.mdi-amazon-drive::before {
+ content: "\F02E";
+}
+.mdi-ambulance::before {
+ content: "\F02F";
+}
+.mdi-ammunition::before {
+ content: "\FCC4";
+}
+.mdi-ampersand::before {
+ content: "\FA8C";
+}
+.mdi-amplifier::before {
+ content: "\F030";
+}
+.mdi-amplifier-off::before {
+ content: "\F01E0";
+}
+.mdi-anchor::before {
+ content: "\F031";
+}
+.mdi-android::before {
+ content: "\F032";
+}
+.mdi-android-auto::before {
+ content: "\FA8D";
+}
+.mdi-android-debug-bridge::before {
+ content: "\F033";
+}
+.mdi-android-head::before {
+ content: "\F78F";
+}
+.mdi-android-messages::before {
+ content: "\FD21";
+}
+.mdi-android-studio::before {
+ content: "\F034";
+}
+.mdi-angle-acute::before {
+ content: "\F936";
+}
+.mdi-angle-obtuse::before {
+ content: "\F937";
+}
+.mdi-angle-right::before {
+ content: "\F938";
+}
+.mdi-angular::before {
+ content: "\F6B1";
+}
+.mdi-angularjs::before {
+ content: "\F6BE";
+}
+.mdi-animation::before {
+ content: "\F5D8";
+}
+.mdi-animation-outline::before {
+ content: "\FA8E";
+}
+.mdi-animation-play::before {
+ content: "\F939";
+}
+.mdi-animation-play-outline::before {
+ content: "\FA8F";
+}
+.mdi-ansible::before {
+ content: "\F00C5";
+}
+.mdi-antenna::before {
+ content: "\F0144";
+}
+.mdi-anvil::before {
+ content: "\F89A";
+}
+.mdi-apache-kafka::before {
+ content: "\F0031";
+}
+.mdi-api::before {
+ content: "\F00C6";
+}
+.mdi-api-off::before {
+ content: "\F0282";
+}
+.mdi-apple::before {
+ content: "\F035";
+}
+.mdi-apple-finder::before {
+ content: "\F036";
+}
+.mdi-apple-icloud::before {
+ content: "\F038";
+}
+.mdi-apple-ios::before {
+ content: "\F037";
+}
+.mdi-apple-keyboard-caps::before {
+ content: "\F632";
+}
+.mdi-apple-keyboard-command::before {
+ content: "\F633";
+}
+.mdi-apple-keyboard-control::before {
+ content: "\F634";
+}
+.mdi-apple-keyboard-option::before {
+ content: "\F635";
+}
+.mdi-apple-keyboard-shift::before {
+ content: "\F636";
+}
+.mdi-apple-safari::before {
+ content: "\F039";
+}
+.mdi-application::before {
+ content: "\F614";
+}
+.mdi-application-export::before {
+ content: "\FD89";
+}
+.mdi-application-import::before {
+ content: "\FD8A";
+}
+.mdi-approximately-equal::before {
+ content: "\FFBE";
+}
+.mdi-approximately-equal-box::before {
+ content: "\FFBF";
+}
+.mdi-apps::before {
+ content: "\F03B";
+}
+.mdi-apps-box::before {
+ content: "\FD22";
+}
+.mdi-arch::before {
+ content: "\F8C6";
+}
+.mdi-archive::before {
+ content: "\F03C";
+}
+.mdi-archive-arrow-down::before {
+ content: "\F0284";
+}
+.mdi-archive-arrow-down-outline::before {
+ content: "\F0285";
+}
+.mdi-archive-arrow-up::before {
+ content: "\F0286";
+}
+.mdi-archive-arrow-up-outline::before {
+ content: "\F0287";
+}
+.mdi-archive-outline::before {
+ content: "\F0239";
+}
+.mdi-arm-flex::before {
+ content: "\F008F";
+}
+.mdi-arm-flex-outline::before {
+ content: "\F0090";
+}
+.mdi-arrange-bring-forward::before {
+ content: "\F03D";
+}
+.mdi-arrange-bring-to-front::before {
+ content: "\F03E";
+}
+.mdi-arrange-send-backward::before {
+ content: "\F03F";
+}
+.mdi-arrange-send-to-back::before {
+ content: "\F040";
+}
+.mdi-arrow-all::before {
+ content: "\F041";
+}
+.mdi-arrow-bottom-left::before {
+ content: "\F042";
+}
+.mdi-arrow-bottom-left-bold-outline::before {
+ content: "\F9B6";
+}
+.mdi-arrow-bottom-left-thick::before {
+ content: "\F9B7";
+}
+.mdi-arrow-bottom-right::before {
+ content: "\F043";
+}
+.mdi-arrow-bottom-right-bold-outline::before {
+ content: "\F9B8";
+}
+.mdi-arrow-bottom-right-thick::before {
+ content: "\F9B9";
+}
+.mdi-arrow-collapse::before {
+ content: "\F615";
+}
+.mdi-arrow-collapse-all::before {
+ content: "\F044";
+}
+.mdi-arrow-collapse-down::before {
+ content: "\F791";
+}
+.mdi-arrow-collapse-horizontal::before {
+ content: "\F84B";
+}
+.mdi-arrow-collapse-left::before {
+ content: "\F792";
+}
+.mdi-arrow-collapse-right::before {
+ content: "\F793";
+}
+.mdi-arrow-collapse-up::before {
+ content: "\F794";
+}
+.mdi-arrow-collapse-vertical::before {
+ content: "\F84C";
+}
+.mdi-arrow-decision::before {
+ content: "\F9BA";
+}
+.mdi-arrow-decision-auto::before {
+ content: "\F9BB";
+}
+.mdi-arrow-decision-auto-outline::before {
+ content: "\F9BC";
+}
+.mdi-arrow-decision-outline::before {
+ content: "\F9BD";
+}
+.mdi-arrow-down::before {
+ content: "\F045";
+}
+.mdi-arrow-down-bold::before {
+ content: "\F72D";
+}
+.mdi-arrow-down-bold-box::before {
+ content: "\F72E";
+}
+.mdi-arrow-down-bold-box-outline::before {
+ content: "\F72F";
+}
+.mdi-arrow-down-bold-circle::before {
+ content: "\F047";
+}
+.mdi-arrow-down-bold-circle-outline::before {
+ content: "\F048";
+}
+.mdi-arrow-down-bold-hexagon-outline::before {
+ content: "\F049";
+}
+.mdi-arrow-down-bold-outline::before {
+ content: "\F9BE";
+}
+.mdi-arrow-down-box::before {
+ content: "\F6BF";
+}
+.mdi-arrow-down-circle::before {
+ content: "\FCB7";
+}
+.mdi-arrow-down-circle-outline::before {
+ content: "\FCB8";
+}
+.mdi-arrow-down-drop-circle::before {
+ content: "\F04A";
+}
+.mdi-arrow-down-drop-circle-outline::before {
+ content: "\F04B";
+}
+.mdi-arrow-down-thick::before {
+ content: "\F046";
+}
+.mdi-arrow-expand::before {
+ content: "\F616";
+}
+.mdi-arrow-expand-all::before {
+ content: "\F04C";
+}
+.mdi-arrow-expand-down::before {
+ content: "\F795";
+}
+.mdi-arrow-expand-horizontal::before {
+ content: "\F84D";
+}
+.mdi-arrow-expand-left::before {
+ content: "\F796";
+}
+.mdi-arrow-expand-right::before {
+ content: "\F797";
+}
+.mdi-arrow-expand-up::before {
+ content: "\F798";
+}
+.mdi-arrow-expand-vertical::before {
+ content: "\F84E";
+}
+.mdi-arrow-horizontal-lock::before {
+ content: "\F0186";
+}
+.mdi-arrow-left::before {
+ content: "\F04D";
+}
+.mdi-arrow-left-bold::before {
+ content: "\F730";
+}
+.mdi-arrow-left-bold-box::before {
+ content: "\F731";
+}
+.mdi-arrow-left-bold-box-outline::before {
+ content: "\F732";
+}
+.mdi-arrow-left-bold-circle::before {
+ content: "\F04F";
+}
+.mdi-arrow-left-bold-circle-outline::before {
+ content: "\F050";
+}
+.mdi-arrow-left-bold-hexagon-outline::before {
+ content: "\F051";
+}
+.mdi-arrow-left-bold-outline::before {
+ content: "\F9BF";
+}
+.mdi-arrow-left-box::before {
+ content: "\F6C0";
+}
+.mdi-arrow-left-circle::before {
+ content: "\FCB9";
+}
+.mdi-arrow-left-circle-outline::before {
+ content: "\FCBA";
+}
+.mdi-arrow-left-drop-circle::before {
+ content: "\F052";
+}
+.mdi-arrow-left-drop-circle-outline::before {
+ content: "\F053";
+}
+.mdi-arrow-left-right::before {
+ content: "\FE90";
+}
+.mdi-arrow-left-right-bold::before {
+ content: "\FE91";
+}
+.mdi-arrow-left-right-bold-outline::before {
+ content: "\F9C0";
+}
+.mdi-arrow-left-thick::before {
+ content: "\F04E";
+}
+.mdi-arrow-right::before {
+ content: "\F054";
+}
+.mdi-arrow-right-bold::before {
+ content: "\F733";
+}
+.mdi-arrow-right-bold-box::before {
+ content: "\F734";
+}
+.mdi-arrow-right-bold-box-outline::before {
+ content: "\F735";
+}
+.mdi-arrow-right-bold-circle::before {
+ content: "\F056";
+}
+.mdi-arrow-right-bold-circle-outline::before {
+ content: "\F057";
+}
+.mdi-arrow-right-bold-hexagon-outline::before {
+ content: "\F058";
+}
+.mdi-arrow-right-bold-outline::before {
+ content: "\F9C1";
+}
+.mdi-arrow-right-box::before {
+ content: "\F6C1";
+}
+.mdi-arrow-right-circle::before {
+ content: "\FCBB";
+}
+.mdi-arrow-right-circle-outline::before {
+ content: "\FCBC";
+}
+.mdi-arrow-right-drop-circle::before {
+ content: "\F059";
+}
+.mdi-arrow-right-drop-circle-outline::before {
+ content: "\F05A";
+}
+.mdi-arrow-right-thick::before {
+ content: "\F055";
+}
+.mdi-arrow-split-horizontal::before {
+ content: "\F93A";
+}
+.mdi-arrow-split-vertical::before {
+ content: "\F93B";
+}
+.mdi-arrow-top-left::before {
+ content: "\F05B";
+}
+.mdi-arrow-top-left-bold-outline::before {
+ content: "\F9C2";
+}
+.mdi-arrow-top-left-bottom-right::before {
+ content: "\FE92";
+}
+.mdi-arrow-top-left-bottom-right-bold::before {
+ content: "\FE93";
+}
+.mdi-arrow-top-left-thick::before {
+ content: "\F9C3";
+}
+.mdi-arrow-top-right::before {
+ content: "\F05C";
+}
+.mdi-arrow-top-right-bold-outline::before {
+ content: "\F9C4";
+}
+.mdi-arrow-top-right-bottom-left::before {
+ content: "\FE94";
+}
+.mdi-arrow-top-right-bottom-left-bold::before {
+ content: "\FE95";
+}
+.mdi-arrow-top-right-thick::before {
+ content: "\F9C5";
+}
+.mdi-arrow-up::before {
+ content: "\F05D";
+}
+.mdi-arrow-up-bold::before {
+ content: "\F736";
+}
+.mdi-arrow-up-bold-box::before {
+ content: "\F737";
+}
+.mdi-arrow-up-bold-box-outline::before {
+ content: "\F738";
+}
+.mdi-arrow-up-bold-circle::before {
+ content: "\F05F";
+}
+.mdi-arrow-up-bold-circle-outline::before {
+ content: "\F060";
+}
+.mdi-arrow-up-bold-hexagon-outline::before {
+ content: "\F061";
+}
+.mdi-arrow-up-bold-outline::before {
+ content: "\F9C6";
+}
+.mdi-arrow-up-box::before {
+ content: "\F6C2";
+}
+.mdi-arrow-up-circle::before {
+ content: "\FCBD";
+}
+.mdi-arrow-up-circle-outline::before {
+ content: "\FCBE";
+}
+.mdi-arrow-up-down::before {
+ content: "\FE96";
+}
+.mdi-arrow-up-down-bold::before {
+ content: "\FE97";
+}
+.mdi-arrow-up-down-bold-outline::before {
+ content: "\F9C7";
+}
+.mdi-arrow-up-drop-circle::before {
+ content: "\F062";
+}
+.mdi-arrow-up-drop-circle-outline::before {
+ content: "\F063";
+}
+.mdi-arrow-up-thick::before {
+ content: "\F05E";
+}
+.mdi-arrow-vertical-lock::before {
+ content: "\F0187";
+}
+.mdi-artist::before {
+ content: "\F802";
+}
+.mdi-artist-outline::before {
+ content: "\FCC5";
+}
+.mdi-artstation::before {
+ content: "\FB37";
+}
+.mdi-aspect-ratio::before {
+ content: "\FA23";
+}
+.mdi-assistant::before {
+ content: "\F064";
+}
+.mdi-asterisk::before {
+ content: "\F6C3";
+}
+.mdi-at::before {
+ content: "\F065";
+}
+.mdi-atlassian::before {
+ content: "\F803";
+}
+.mdi-atm::before {
+ content: "\FD23";
+}
+.mdi-atom::before {
+ content: "\F767";
+}
+.mdi-atom-variant::before {
+ content: "\FE98";
+}
+.mdi-attachment::before {
+ content: "\F066";
+}
+.mdi-audio-video::before {
+ content: "\F93C";
+}
+.mdi-audio-video-off::before {
+ content: "\F01E1";
+}
+.mdi-audiobook::before {
+ content: "\F067";
+}
+.mdi-augmented-reality::before {
+ content: "\F84F";
+}
+.mdi-auto-download::before {
+ content: "\F03A9";
+}
+.mdi-auto-fix::before {
+ content: "\F068";
+}
+.mdi-auto-upload::before {
+ content: "\F069";
+}
+.mdi-autorenew::before {
+ content: "\F06A";
+}
+.mdi-av-timer::before {
+ content: "\F06B";
+}
+.mdi-aws::before {
+ content: "\FDF2";
+}
+.mdi-axe::before {
+ content: "\F8C7";
+}
+.mdi-axis::before {
+ content: "\FD24";
+}
+.mdi-axis-arrow::before {
+ content: "\FD25";
+}
+.mdi-axis-arrow-lock::before {
+ content: "\FD26";
+}
+.mdi-axis-lock::before {
+ content: "\FD27";
+}
+.mdi-axis-x-arrow::before {
+ content: "\FD28";
+}
+.mdi-axis-x-arrow-lock::before {
+ content: "\FD29";
+}
+.mdi-axis-x-rotate-clockwise::before {
+ content: "\FD2A";
+}
+.mdi-axis-x-rotate-counterclockwise::before {
+ content: "\FD2B";
+}
+.mdi-axis-x-y-arrow-lock::before {
+ content: "\FD2C";
+}
+.mdi-axis-y-arrow::before {
+ content: "\FD2D";
+}
+.mdi-axis-y-arrow-lock::before {
+ content: "\FD2E";
+}
+.mdi-axis-y-rotate-clockwise::before {
+ content: "\FD2F";
+}
+.mdi-axis-y-rotate-counterclockwise::before {
+ content: "\FD30";
+}
+.mdi-axis-z-arrow::before {
+ content: "\FD31";
+}
+.mdi-axis-z-arrow-lock::before {
+ content: "\FD32";
+}
+.mdi-axis-z-rotate-clockwise::before {
+ content: "\FD33";
+}
+.mdi-axis-z-rotate-counterclockwise::before {
+ content: "\FD34";
+}
+.mdi-azure::before {
+ content: "\F804";
+}
+.mdi-azure-devops::before {
+ content: "\F0091";
+}
+.mdi-babel::before {
+ content: "\FA24";
+}
+.mdi-baby::before {
+ content: "\F06C";
+}
+.mdi-baby-bottle::before {
+ content: "\FF56";
+}
+.mdi-baby-bottle-outline::before {
+ content: "\FF57";
+}
+.mdi-baby-carriage::before {
+ content: "\F68E";
+}
+.mdi-baby-carriage-off::before {
+ content: "\FFC0";
+}
+.mdi-baby-face::before {
+ content: "\FE99";
+}
+.mdi-baby-face-outline::before {
+ content: "\FE9A";
+}
+.mdi-backburger::before {
+ content: "\F06D";
+}
+.mdi-backspace::before {
+ content: "\F06E";
+}
+.mdi-backspace-outline::before {
+ content: "\FB38";
+}
+.mdi-backspace-reverse::before {
+ content: "\FE9B";
+}
+.mdi-backspace-reverse-outline::before {
+ content: "\FE9C";
+}
+.mdi-backup-restore::before {
+ content: "\F06F";
+}
+.mdi-bacteria::before {
+ content: "\FEF2";
+}
+.mdi-bacteria-outline::before {
+ content: "\FEF3";
+}
+.mdi-badminton::before {
+ content: "\F850";
+}
+.mdi-bag-carry-on::before {
+ content: "\FF58";
+}
+.mdi-bag-carry-on-check::before {
+ content: "\FD41";
+}
+.mdi-bag-carry-on-off::before {
+ content: "\FF59";
+}
+.mdi-bag-checked::before {
+ content: "\FF5A";
+}
+.mdi-bag-personal::before {
+ content: "\FDF3";
+}
+.mdi-bag-personal-off::before {
+ content: "\FDF4";
+}
+.mdi-bag-personal-off-outline::before {
+ content: "\FDF5";
+}
+.mdi-bag-personal-outline::before {
+ content: "\FDF6";
+}
+.mdi-baguette::before {
+ content: "\FF5B";
+}
+.mdi-balloon::before {
+ content: "\FA25";
+}
+.mdi-ballot::before {
+ content: "\F9C8";
+}
+.mdi-ballot-outline::before {
+ content: "\F9C9";
+}
+.mdi-ballot-recount::before {
+ content: "\FC15";
+}
+.mdi-ballot-recount-outline::before {
+ content: "\FC16";
+}
+.mdi-bandage::before {
+ content: "\FD8B";
+}
+.mdi-bandcamp::before {
+ content: "\F674";
+}
+.mdi-bank::before {
+ content: "\F070";
+}
+.mdi-bank-minus::before {
+ content: "\FD8C";
+}
+.mdi-bank-outline::before {
+ content: "\FE9D";
+}
+.mdi-bank-plus::before {
+ content: "\FD8D";
+}
+.mdi-bank-remove::before {
+ content: "\FD8E";
+}
+.mdi-bank-transfer::before {
+ content: "\FA26";
+}
+.mdi-bank-transfer-in::before {
+ content: "\FA27";
+}
+.mdi-bank-transfer-out::before {
+ content: "\FA28";
+}
+.mdi-barcode::before {
+ content: "\F071";
+}
+.mdi-barcode-off::before {
+ content: "\F0261";
+}
+.mdi-barcode-scan::before {
+ content: "\F072";
+}
+.mdi-barley::before {
+ content: "\F073";
+}
+.mdi-barley-off::before {
+ content: "\FB39";
+}
+.mdi-barn::before {
+ content: "\FB3A";
+}
+.mdi-barrel::before {
+ content: "\F074";
+}
+.mdi-baseball::before {
+ content: "\F851";
+}
+.mdi-baseball-bat::before {
+ content: "\F852";
+}
+.mdi-basecamp::before {
+ content: "\F075";
+}
+.mdi-bash::before {
+ content: "\F01AE";
+}
+.mdi-basket::before {
+ content: "\F076";
+}
+.mdi-basket-fill::before {
+ content: "\F077";
+}
+.mdi-basket-outline::before {
+ content: "\F01AC";
+}
+.mdi-basket-unfill::before {
+ content: "\F078";
+}
+.mdi-basketball::before {
+ content: "\F805";
+}
+.mdi-basketball-hoop::before {
+ content: "\FC17";
+}
+.mdi-basketball-hoop-outline::before {
+ content: "\FC18";
+}
+.mdi-bat::before {
+ content: "\FB3B";
+}
+.mdi-battery::before {
+ content: "\F079";
+}
+.mdi-battery-10::before {
+ content: "\F07A";
+}
+.mdi-battery-10-bluetooth::before {
+ content: "\F93D";
+}
+.mdi-battery-20::before {
+ content: "\F07B";
+}
+.mdi-battery-20-bluetooth::before {
+ content: "\F93E";
+}
+.mdi-battery-30::before {
+ content: "\F07C";
+}
+.mdi-battery-30-bluetooth::before {
+ content: "\F93F";
+}
+.mdi-battery-40::before {
+ content: "\F07D";
+}
+.mdi-battery-40-bluetooth::before {
+ content: "\F940";
+}
+.mdi-battery-50::before {
+ content: "\F07E";
+}
+.mdi-battery-50-bluetooth::before {
+ content: "\F941";
+}
+.mdi-battery-60::before {
+ content: "\F07F";
+}
+.mdi-battery-60-bluetooth::before {
+ content: "\F942";
+}
+.mdi-battery-70::before {
+ content: "\F080";
+}
+.mdi-battery-70-bluetooth::before {
+ content: "\F943";
+}
+.mdi-battery-80::before {
+ content: "\F081";
+}
+.mdi-battery-80-bluetooth::before {
+ content: "\F944";
+}
+.mdi-battery-90::before {
+ content: "\F082";
+}
+.mdi-battery-90-bluetooth::before {
+ content: "\F945";
+}
+.mdi-battery-alert::before {
+ content: "\F083";
+}
+.mdi-battery-alert-bluetooth::before {
+ content: "\F946";
+}
+.mdi-battery-alert-variant::before {
+ content: "\F00F7";
+}
+.mdi-battery-alert-variant-outline::before {
+ content: "\F00F8";
+}
+.mdi-battery-bluetooth::before {
+ content: "\F947";
+}
+.mdi-battery-bluetooth-variant::before {
+ content: "\F948";
+}
+.mdi-battery-charging::before {
+ content: "\F084";
+}
+.mdi-battery-charging-10::before {
+ content: "\F89B";
+}
+.mdi-battery-charging-100::before {
+ content: "\F085";
+}
+.mdi-battery-charging-20::before {
+ content: "\F086";
+}
+.mdi-battery-charging-30::before {
+ content: "\F087";
+}
+.mdi-battery-charging-40::before {
+ content: "\F088";
+}
+.mdi-battery-charging-50::before {
+ content: "\F89C";
+}
+.mdi-battery-charging-60::before {
+ content: "\F089";
+}
+.mdi-battery-charging-70::before {
+ content: "\F89D";
+}
+.mdi-battery-charging-80::before {
+ content: "\F08A";
+}
+.mdi-battery-charging-90::before {
+ content: "\F08B";
+}
+.mdi-battery-charging-high::before {
+ content: "\F02D1";
+}
+.mdi-battery-charging-low::before {
+ content: "\F02CF";
+}
+.mdi-battery-charging-medium::before {
+ content: "\F02D0";
+}
+.mdi-battery-charging-outline::before {
+ content: "\F89E";
+}
+.mdi-battery-charging-wireless::before {
+ content: "\F806";
+}
+.mdi-battery-charging-wireless-10::before {
+ content: "\F807";
+}
+.mdi-battery-charging-wireless-20::before {
+ content: "\F808";
+}
+.mdi-battery-charging-wireless-30::before {
+ content: "\F809";
+}
+.mdi-battery-charging-wireless-40::before {
+ content: "\F80A";
+}
+.mdi-battery-charging-wireless-50::before {
+ content: "\F80B";
+}
+.mdi-battery-charging-wireless-60::before {
+ content: "\F80C";
+}
+.mdi-battery-charging-wireless-70::before {
+ content: "\F80D";
+}
+.mdi-battery-charging-wireless-80::before {
+ content: "\F80E";
+}
+.mdi-battery-charging-wireless-90::before {
+ content: "\F80F";
+}
+.mdi-battery-charging-wireless-alert::before {
+ content: "\F810";
+}
+.mdi-battery-charging-wireless-outline::before {
+ content: "\F811";
+}
+.mdi-battery-heart::before {
+ content: "\F023A";
+}
+.mdi-battery-heart-outline::before {
+ content: "\F023B";
+}
+.mdi-battery-heart-variant::before {
+ content: "\F023C";
+}
+.mdi-battery-high::before {
+ content: "\F02CE";
+}
+.mdi-battery-low::before {
+ content: "\F02CC";
+}
+.mdi-battery-medium::before {
+ content: "\F02CD";
+}
+.mdi-battery-minus::before {
+ content: "\F08C";
+}
+.mdi-battery-negative::before {
+ content: "\F08D";
+}
+.mdi-battery-off::before {
+ content: "\F0288";
+}
+.mdi-battery-off-outline::before {
+ content: "\F0289";
+}
+.mdi-battery-outline::before {
+ content: "\F08E";
+}
+.mdi-battery-plus::before {
+ content: "\F08F";
+}
+.mdi-battery-positive::before {
+ content: "\F090";
+}
+.mdi-battery-unknown::before {
+ content: "\F091";
+}
+.mdi-battery-unknown-bluetooth::before {
+ content: "\F949";
+}
+.mdi-battlenet::before {
+ content: "\FB3C";
+}
+.mdi-beach::before {
+ content: "\F092";
+}
+.mdi-beaker::before {
+ content: "\FCC6";
+}
+.mdi-beaker-alert::before {
+ content: "\F0254";
+}
+.mdi-beaker-alert-outline::before {
+ content: "\F0255";
+}
+.mdi-beaker-check::before {
+ content: "\F0256";
+}
+.mdi-beaker-check-outline::before {
+ content: "\F0257";
+}
+.mdi-beaker-minus::before {
+ content: "\F0258";
+}
+.mdi-beaker-minus-outline::before {
+ content: "\F0259";
+}
+.mdi-beaker-outline::before {
+ content: "\F68F";
+}
+.mdi-beaker-plus::before {
+ content: "\F025A";
+}
+.mdi-beaker-plus-outline::before {
+ content: "\F025B";
+}
+.mdi-beaker-question::before {
+ content: "\F025C";
+}
+.mdi-beaker-question-outline::before {
+ content: "\F025D";
+}
+.mdi-beaker-remove::before {
+ content: "\F025E";
+}
+.mdi-beaker-remove-outline::before {
+ content: "\F025F";
+}
+.mdi-beats::before {
+ content: "\F097";
+}
+.mdi-bed-double::before {
+ content: "\F0092";
+}
+.mdi-bed-double-outline::before {
+ content: "\F0093";
+}
+.mdi-bed-empty::before {
+ content: "\F89F";
+}
+.mdi-bed-king::before {
+ content: "\F0094";
+}
+.mdi-bed-king-outline::before {
+ content: "\F0095";
+}
+.mdi-bed-queen::before {
+ content: "\F0096";
+}
+.mdi-bed-queen-outline::before {
+ content: "\F0097";
+}
+.mdi-bed-single::before {
+ content: "\F0098";
+}
+.mdi-bed-single-outline::before {
+ content: "\F0099";
+}
+.mdi-bee::before {
+ content: "\FFC1";
+}
+.mdi-bee-flower::before {
+ content: "\FFC2";
+}
+.mdi-beehive-outline::before {
+ content: "\F00F9";
+}
+.mdi-beer::before {
+ content: "\F098";
+}
+.mdi-beer-outline::before {
+ content: "\F0337";
+}
+.mdi-behance::before {
+ content: "\F099";
+}
+.mdi-bell::before {
+ content: "\F09A";
+}
+.mdi-bell-alert::before {
+ content: "\FD35";
+}
+.mdi-bell-alert-outline::before {
+ content: "\FE9E";
+}
+.mdi-bell-check::before {
+ content: "\F0210";
+}
+.mdi-bell-check-outline::before {
+ content: "\F0211";
+}
+.mdi-bell-circle::before {
+ content: "\FD36";
+}
+.mdi-bell-circle-outline::before {
+ content: "\FD37";
+}
+.mdi-bell-off::before {
+ content: "\F09B";
+}
+.mdi-bell-off-outline::before {
+ content: "\FA90";
+}
+.mdi-bell-outline::before {
+ content: "\F09C";
+}
+.mdi-bell-plus::before {
+ content: "\F09D";
+}
+.mdi-bell-plus-outline::before {
+ content: "\FA91";
+}
+.mdi-bell-ring::before {
+ content: "\F09E";
+}
+.mdi-bell-ring-outline::before {
+ content: "\F09F";
+}
+.mdi-bell-sleep::before {
+ content: "\F0A0";
+}
+.mdi-bell-sleep-outline::before {
+ content: "\FA92";
+}
+.mdi-beta::before {
+ content: "\F0A1";
+}
+.mdi-betamax::before {
+ content: "\F9CA";
+}
+.mdi-biathlon::before {
+ content: "\FDF7";
+}
+.mdi-bible::before {
+ content: "\F0A2";
+}
+.mdi-bicycle::before {
+ content: "\F00C7";
+}
+.mdi-bicycle-basket::before {
+ content: "\F0260";
+}
+.mdi-bike::before {
+ content: "\F0A3";
+}
+.mdi-bike-fast::before {
+ content: "\F014A";
+}
+.mdi-billboard::before {
+ content: "\F0032";
+}
+.mdi-billiards::before {
+ content: "\FB3D";
+}
+.mdi-billiards-rack::before {
+ content: "\FB3E";
+}
+.mdi-bing::before {
+ content: "\F0A4";
+}
+.mdi-binoculars::before {
+ content: "\F0A5";
+}
+.mdi-bio::before {
+ content: "\F0A6";
+}
+.mdi-biohazard::before {
+ content: "\F0A7";
+}
+.mdi-bitbucket::before {
+ content: "\F0A8";
+}
+.mdi-bitcoin::before {
+ content: "\F812";
+}
+.mdi-black-mesa::before {
+ content: "\F0A9";
+}
+.mdi-blackberry::before {
+ content: "\F0AA";
+}
+.mdi-blender::before {
+ content: "\FCC7";
+}
+.mdi-blender-software::before {
+ content: "\F0AB";
+}
+.mdi-blinds::before {
+ content: "\F0AC";
+}
+.mdi-blinds-open::before {
+ content: "\F0033";
+}
+.mdi-block-helper::before {
+ content: "\F0AD";
+}
+.mdi-blogger::before {
+ content: "\F0AE";
+}
+.mdi-blood-bag::before {
+ content: "\FCC8";
+}
+.mdi-bluetooth::before {
+ content: "\F0AF";
+}
+.mdi-bluetooth-audio::before {
+ content: "\F0B0";
+}
+.mdi-bluetooth-connect::before {
+ content: "\F0B1";
+}
+.mdi-bluetooth-off::before {
+ content: "\F0B2";
+}
+.mdi-bluetooth-settings::before {
+ content: "\F0B3";
+}
+.mdi-bluetooth-transfer::before {
+ content: "\F0B4";
+}
+.mdi-blur::before {
+ content: "\F0B5";
+}
+.mdi-blur-linear::before {
+ content: "\F0B6";
+}
+.mdi-blur-off::before {
+ content: "\F0B7";
+}
+.mdi-blur-radial::before {
+ content: "\F0B8";
+}
+.mdi-bolnisi-cross::before {
+ content: "\FCC9";
+}
+.mdi-bolt::before {
+ content: "\FD8F";
+}
+.mdi-bomb::before {
+ content: "\F690";
+}
+.mdi-bomb-off::before {
+ content: "\F6C4";
+}
+.mdi-bone::before {
+ content: "\F0B9";
+}
+.mdi-book::before {
+ content: "\F0BA";
+}
+.mdi-book-information-variant::before {
+ content: "\F009A";
+}
+.mdi-book-lock::before {
+ content: "\F799";
+}
+.mdi-book-lock-open::before {
+ content: "\F79A";
+}
+.mdi-book-minus::before {
+ content: "\F5D9";
+}
+.mdi-book-minus-multiple::before {
+ content: "\FA93";
+}
+.mdi-book-multiple::before {
+ content: "\F0BB";
+}
+.mdi-book-open::before {
+ content: "\F0BD";
+}
+.mdi-book-open-outline::before {
+ content: "\FB3F";
+}
+.mdi-book-open-page-variant::before {
+ content: "\F5DA";
+}
+.mdi-book-open-variant::before {
+ content: "\F0BE";
+}
+.mdi-book-outline::before {
+ content: "\FB40";
+}
+.mdi-book-play::before {
+ content: "\FE9F";
+}
+.mdi-book-play-outline::before {
+ content: "\FEA0";
+}
+.mdi-book-plus::before {
+ content: "\F5DB";
+}
+.mdi-book-plus-multiple::before {
+ content: "\FA94";
+}
+.mdi-book-remove::before {
+ content: "\FA96";
+}
+.mdi-book-remove-multiple::before {
+ content: "\FA95";
+}
+.mdi-book-search::before {
+ content: "\FEA1";
+}
+.mdi-book-search-outline::before {
+ content: "\FEA2";
+}
+.mdi-book-variant::before {
+ content: "\F0BF";
+}
+.mdi-book-variant-multiple::before {
+ content: "\F0BC";
+}
+.mdi-bookmark::before {
+ content: "\F0C0";
+}
+.mdi-bookmark-check::before {
+ content: "\F0C1";
+}
+.mdi-bookmark-check-outline::before {
+ content: "\F03A6";
+}
+.mdi-bookmark-minus::before {
+ content: "\F9CB";
+}
+.mdi-bookmark-minus-outline::before {
+ content: "\F9CC";
+}
+.mdi-bookmark-multiple::before {
+ content: "\FDF8";
+}
+.mdi-bookmark-multiple-outline::before {
+ content: "\FDF9";
+}
+.mdi-bookmark-music::before {
+ content: "\F0C2";
+}
+.mdi-bookmark-music-outline::before {
+ content: "\F03A4";
+}
+.mdi-bookmark-off::before {
+ content: "\F9CD";
+}
+.mdi-bookmark-off-outline::before {
+ content: "\F9CE";
+}
+.mdi-bookmark-outline::before {
+ content: "\F0C3";
+}
+.mdi-bookmark-plus::before {
+ content: "\F0C5";
+}
+.mdi-bookmark-plus-outline::before {
+ content: "\F0C4";
+}
+.mdi-bookmark-remove::before {
+ content: "\F0C6";
+}
+.mdi-bookmark-remove-outline::before {
+ content: "\F03A5";
+}
+.mdi-bookshelf::before {
+ content: "\F028A";
+}
+.mdi-boom-gate::before {
+ content: "\FEA3";
+}
+.mdi-boom-gate-alert::before {
+ content: "\FEA4";
+}
+.mdi-boom-gate-alert-outline::before {
+ content: "\FEA5";
+}
+.mdi-boom-gate-down::before {
+ content: "\FEA6";
+}
+.mdi-boom-gate-down-outline::before {
+ content: "\FEA7";
+}
+.mdi-boom-gate-outline::before {
+ content: "\FEA8";
+}
+.mdi-boom-gate-up::before {
+ content: "\FEA9";
+}
+.mdi-boom-gate-up-outline::before {
+ content: "\FEAA";
+}
+.mdi-boombox::before {
+ content: "\F5DC";
+}
+.mdi-boomerang::before {
+ content: "\F00FA";
+}
+.mdi-bootstrap::before {
+ content: "\F6C5";
+}
+.mdi-border-all::before {
+ content: "\F0C7";
+}
+.mdi-border-all-variant::before {
+ content: "\F8A0";
+}
+.mdi-border-bottom::before {
+ content: "\F0C8";
+}
+.mdi-border-bottom-variant::before {
+ content: "\F8A1";
+}
+.mdi-border-color::before {
+ content: "\F0C9";
+}
+.mdi-border-horizontal::before {
+ content: "\F0CA";
+}
+.mdi-border-inside::before {
+ content: "\F0CB";
+}
+.mdi-border-left::before {
+ content: "\F0CC";
+}
+.mdi-border-left-variant::before {
+ content: "\F8A2";
+}
+.mdi-border-none::before {
+ content: "\F0CD";
+}
+.mdi-border-none-variant::before {
+ content: "\F8A3";
+}
+.mdi-border-outside::before {
+ content: "\F0CE";
+}
+.mdi-border-right::before {
+ content: "\F0CF";
+}
+.mdi-border-right-variant::before {
+ content: "\F8A4";
+}
+.mdi-border-style::before {
+ content: "\F0D0";
+}
+.mdi-border-top::before {
+ content: "\F0D1";
+}
+.mdi-border-top-variant::before {
+ content: "\F8A5";
+}
+.mdi-border-vertical::before {
+ content: "\F0D2";
+}
+.mdi-bottle-soda::before {
+ content: "\F009B";
+}
+.mdi-bottle-soda-classic::before {
+ content: "\F009C";
+}
+.mdi-bottle-soda-classic-outline::before {
+ content: "\F038E";
+}
+.mdi-bottle-soda-outline::before {
+ content: "\F009D";
+}
+.mdi-bottle-tonic::before {
+ content: "\F0159";
+}
+.mdi-bottle-tonic-outline::before {
+ content: "\F015A";
+}
+.mdi-bottle-tonic-plus::before {
+ content: "\F015B";
+}
+.mdi-bottle-tonic-plus-outline::before {
+ content: "\F015C";
+}
+.mdi-bottle-tonic-skull::before {
+ content: "\F015D";
+}
+.mdi-bottle-tonic-skull-outline::before {
+ content: "\F015E";
+}
+.mdi-bottle-wine::before {
+ content: "\F853";
+}
+.mdi-bottle-wine-outline::before {
+ content: "\F033B";
+}
+.mdi-bow-tie::before {
+ content: "\F677";
+}
+.mdi-bowl::before {
+ content: "\F617";
+}
+.mdi-bowling::before {
+ content: "\F0D3";
+}
+.mdi-box::before {
+ content: "\F0D4";
+}
+.mdi-box-cutter::before {
+ content: "\F0D5";
+}
+.mdi-box-shadow::before {
+ content: "\F637";
+}
+.mdi-boxing-glove::before {
+ content: "\FB41";
+}
+.mdi-braille::before {
+ content: "\F9CF";
+}
+.mdi-brain::before {
+ content: "\F9D0";
+}
+.mdi-bread-slice::before {
+ content: "\FCCA";
+}
+.mdi-bread-slice-outline::before {
+ content: "\FCCB";
+}
+.mdi-bridge::before {
+ content: "\F618";
+}
+.mdi-briefcase::before {
+ content: "\F0D6";
+}
+.mdi-briefcase-account::before {
+ content: "\FCCC";
+}
+.mdi-briefcase-account-outline::before {
+ content: "\FCCD";
+}
+.mdi-briefcase-check::before {
+ content: "\F0D7";
+}
+.mdi-briefcase-check-outline::before {
+ content: "\F0349";
+}
+.mdi-briefcase-clock::before {
+ content: "\F00FB";
+}
+.mdi-briefcase-clock-outline::before {
+ content: "\F00FC";
+}
+.mdi-briefcase-download::before {
+ content: "\F0D8";
+}
+.mdi-briefcase-download-outline::before {
+ content: "\FC19";
+}
+.mdi-briefcase-edit::before {
+ content: "\FA97";
+}
+.mdi-briefcase-edit-outline::before {
+ content: "\FC1A";
+}
+.mdi-briefcase-minus::before {
+ content: "\FA29";
+}
+.mdi-briefcase-minus-outline::before {
+ content: "\FC1B";
+}
+.mdi-briefcase-outline::before {
+ content: "\F813";
+}
+.mdi-briefcase-plus::before {
+ content: "\FA2A";
+}
+.mdi-briefcase-plus-outline::before {
+ content: "\FC1C";
+}
+.mdi-briefcase-remove::before {
+ content: "\FA2B";
+}
+.mdi-briefcase-remove-outline::before {
+ content: "\FC1D";
+}
+.mdi-briefcase-search::before {
+ content: "\FA2C";
+}
+.mdi-briefcase-search-outline::before {
+ content: "\FC1E";
+}
+.mdi-briefcase-upload::before {
+ content: "\F0D9";
+}
+.mdi-briefcase-upload-outline::before {
+ content: "\FC1F";
+}
+.mdi-brightness-1::before {
+ content: "\F0DA";
+}
+.mdi-brightness-2::before {
+ content: "\F0DB";
+}
+.mdi-brightness-3::before {
+ content: "\F0DC";
+}
+.mdi-brightness-4::before {
+ content: "\F0DD";
+}
+.mdi-brightness-5::before {
+ content: "\F0DE";
+}
+.mdi-brightness-6::before {
+ content: "\F0DF";
+}
+.mdi-brightness-7::before {
+ content: "\F0E0";
+}
+.mdi-brightness-auto::before {
+ content: "\F0E1";
+}
+.mdi-brightness-percent::before {
+ content: "\FCCE";
+}
+.mdi-broom::before {
+ content: "\F0E2";
+}
+.mdi-brush::before {
+ content: "\F0E3";
+}
+.mdi-buddhism::before {
+ content: "\F94A";
+}
+.mdi-buffer::before {
+ content: "\F619";
+}
+.mdi-bug::before {
+ content: "\F0E4";
+}
+.mdi-bug-check::before {
+ content: "\FA2D";
+}
+.mdi-bug-check-outline::before {
+ content: "\FA2E";
+}
+.mdi-bug-outline::before {
+ content: "\FA2F";
+}
+.mdi-bugle::before {
+ content: "\FD90";
+}
+.mdi-bulldozer::before {
+ content: "\FB07";
+}
+.mdi-bullet::before {
+ content: "\FCCF";
+}
+.mdi-bulletin-board::before {
+ content: "\F0E5";
+}
+.mdi-bullhorn::before {
+ content: "\F0E6";
+}
+.mdi-bullhorn-outline::before {
+ content: "\FB08";
+}
+.mdi-bullseye::before {
+ content: "\F5DD";
+}
+.mdi-bullseye-arrow::before {
+ content: "\F8C8";
+}
+.mdi-bulma::before {
+ content: "\F0312";
+}
+.mdi-bunk-bed::before {
+ content: "\F032D";
+}
+.mdi-bus::before {
+ content: "\F0E7";
+}
+.mdi-bus-alert::before {
+ content: "\FA98";
+}
+.mdi-bus-articulated-end::before {
+ content: "\F79B";
+}
+.mdi-bus-articulated-front::before {
+ content: "\F79C";
+}
+.mdi-bus-clock::before {
+ content: "\F8C9";
+}
+.mdi-bus-double-decker::before {
+ content: "\F79D";
+}
+.mdi-bus-marker::before {
+ content: "\F023D";
+}
+.mdi-bus-multiple::before {
+ content: "\FF5C";
+}
+.mdi-bus-school::before {
+ content: "\F79E";
+}
+.mdi-bus-side::before {
+ content: "\F79F";
+}
+.mdi-bus-stop::before {
+ content: "\F0034";
+}
+.mdi-bus-stop-covered::before {
+ content: "\F0035";
+}
+.mdi-bus-stop-uncovered::before {
+ content: "\F0036";
+}
+.mdi-cached::before {
+ content: "\F0E8";
+}
+.mdi-cactus::before {
+ content: "\FD91";
+}
+.mdi-cake::before {
+ content: "\F0E9";
+}
+.mdi-cake-layered::before {
+ content: "\F0EA";
+}
+.mdi-cake-variant::before {
+ content: "\F0EB";
+}
+.mdi-calculator::before {
+ content: "\F0EC";
+}
+.mdi-calculator-variant::before {
+ content: "\FA99";
+}
+.mdi-calendar::before {
+ content: "\F0ED";
+}
+.mdi-calendar-account::before {
+ content: "\FEF4";
+}
+.mdi-calendar-account-outline::before {
+ content: "\FEF5";
+}
+.mdi-calendar-alert::before {
+ content: "\FA30";
+}
+.mdi-calendar-arrow-left::before {
+ content: "\F015F";
+}
+.mdi-calendar-arrow-right::before {
+ content: "\F0160";
+}
+.mdi-calendar-blank::before {
+ content: "\F0EE";
+}
+.mdi-calendar-blank-multiple::before {
+ content: "\F009E";
+}
+.mdi-calendar-blank-outline::before {
+ content: "\FB42";
+}
+.mdi-calendar-check::before {
+ content: "\F0EF";
+}
+.mdi-calendar-check-outline::before {
+ content: "\FC20";
+}
+.mdi-calendar-clock::before {
+ content: "\F0F0";
+}
+.mdi-calendar-edit::before {
+ content: "\F8A6";
+}
+.mdi-calendar-export::before {
+ content: "\FB09";
+}
+.mdi-calendar-heart::before {
+ content: "\F9D1";
+}
+.mdi-calendar-import::before {
+ content: "\FB0A";
+}
+.mdi-calendar-minus::before {
+ content: "\FD38";
+}
+.mdi-calendar-month::before {
+ content: "\FDFA";
+}
+.mdi-calendar-month-outline::before {
+ content: "\FDFB";
+}
+.mdi-calendar-multiple::before {
+ content: "\F0F1";
+}
+.mdi-calendar-multiple-check::before {
+ content: "\F0F2";
+}
+.mdi-calendar-multiselect::before {
+ content: "\FA31";
+}
+.mdi-calendar-outline::before {
+ content: "\FB43";
+}
+.mdi-calendar-plus::before {
+ content: "\F0F3";
+}
+.mdi-calendar-question::before {
+ content: "\F691";
+}
+.mdi-calendar-range::before {
+ content: "\F678";
+}
+.mdi-calendar-range-outline::before {
+ content: "\FB44";
+}
+.mdi-calendar-remove::before {
+ content: "\F0F4";
+}
+.mdi-calendar-remove-outline::before {
+ content: "\FC21";
+}
+.mdi-calendar-repeat::before {
+ content: "\FEAB";
+}
+.mdi-calendar-repeat-outline::before {
+ content: "\FEAC";
+}
+.mdi-calendar-search::before {
+ content: "\F94B";
+}
+.mdi-calendar-star::before {
+ content: "\F9D2";
+}
+.mdi-calendar-text::before {
+ content: "\F0F5";
+}
+.mdi-calendar-text-outline::before {
+ content: "\FC22";
+}
+.mdi-calendar-today::before {
+ content: "\F0F6";
+}
+.mdi-calendar-week::before {
+ content: "\FA32";
+}
+.mdi-calendar-week-begin::before {
+ content: "\FA33";
+}
+.mdi-calendar-weekend::before {
+ content: "\FEF6";
+}
+.mdi-calendar-weekend-outline::before {
+ content: "\FEF7";
+}
+.mdi-call-made::before {
+ content: "\F0F7";
+}
+.mdi-call-merge::before {
+ content: "\F0F8";
+}
+.mdi-call-missed::before {
+ content: "\F0F9";
+}
+.mdi-call-received::before {
+ content: "\F0FA";
+}
+.mdi-call-split::before {
+ content: "\F0FB";
+}
+.mdi-camcorder::before {
+ content: "\F0FC";
+}
+.mdi-camcorder-box::before {
+ content: "\F0FD";
+}
+.mdi-camcorder-box-off::before {
+ content: "\F0FE";
+}
+.mdi-camcorder-off::before {
+ content: "\F0FF";
+}
+.mdi-camera::before {
+ content: "\F100";
+}
+.mdi-camera-account::before {
+ content: "\F8CA";
+}
+.mdi-camera-burst::before {
+ content: "\F692";
+}
+.mdi-camera-control::before {
+ content: "\FB45";
+}
+.mdi-camera-enhance::before {
+ content: "\F101";
+}
+.mdi-camera-enhance-outline::before {
+ content: "\FB46";
+}
+.mdi-camera-front::before {
+ content: "\F102";
+}
+.mdi-camera-front-variant::before {
+ content: "\F103";
+}
+.mdi-camera-gopro::before {
+ content: "\F7A0";
+}
+.mdi-camera-image::before {
+ content: "\F8CB";
+}
+.mdi-camera-iris::before {
+ content: "\F104";
+}
+.mdi-camera-metering-center::before {
+ content: "\F7A1";
+}
+.mdi-camera-metering-matrix::before {
+ content: "\F7A2";
+}
+.mdi-camera-metering-partial::before {
+ content: "\F7A3";
+}
+.mdi-camera-metering-spot::before {
+ content: "\F7A4";
+}
+.mdi-camera-off::before {
+ content: "\F5DF";
+}
+.mdi-camera-outline::before {
+ content: "\FD39";
+}
+.mdi-camera-party-mode::before {
+ content: "\F105";
+}
+.mdi-camera-plus::before {
+ content: "\FEF8";
+}
+.mdi-camera-plus-outline::before {
+ content: "\FEF9";
+}
+.mdi-camera-rear::before {
+ content: "\F106";
+}
+.mdi-camera-rear-variant::before {
+ content: "\F107";
+}
+.mdi-camera-retake::before {
+ content: "\FDFC";
+}
+.mdi-camera-retake-outline::before {
+ content: "\FDFD";
+}
+.mdi-camera-switch::before {
+ content: "\F108";
+}
+.mdi-camera-timer::before {
+ content: "\F109";
+}
+.mdi-camera-wireless::before {
+ content: "\FD92";
+}
+.mdi-camera-wireless-outline::before {
+ content: "\FD93";
+}
+.mdi-campfire::before {
+ content: "\FEFA";
+}
+.mdi-cancel::before {
+ content: "\F739";
+}
+.mdi-candle::before {
+ content: "\F5E2";
+}
+.mdi-candycane::before {
+ content: "\F10A";
+}
+.mdi-cannabis::before {
+ content: "\F7A5";
+}
+.mdi-caps-lock::before {
+ content: "\FA9A";
+}
+.mdi-car::before {
+ content: "\F10B";
+}
+.mdi-car-2-plus::before {
+ content: "\F0037";
+}
+.mdi-car-3-plus::before {
+ content: "\F0038";
+}
+.mdi-car-back::before {
+ content: "\FDFE";
+}
+.mdi-car-battery::before {
+ content: "\F10C";
+}
+.mdi-car-brake-abs::before {
+ content: "\FC23";
+}
+.mdi-car-brake-alert::before {
+ content: "\FC24";
+}
+.mdi-car-brake-hold::before {
+ content: "\FD3A";
+}
+.mdi-car-brake-parking::before {
+ content: "\FD3B";
+}
+.mdi-car-brake-retarder::before {
+ content: "\F0039";
+}
+.mdi-car-child-seat::before {
+ content: "\FFC3";
+}
+.mdi-car-clutch::before {
+ content: "\F003A";
+}
+.mdi-car-connected::before {
+ content: "\F10D";
+}
+.mdi-car-convertible::before {
+ content: "\F7A6";
+}
+.mdi-car-coolant-level::before {
+ content: "\F003B";
+}
+.mdi-car-cruise-control::before {
+ content: "\FD3C";
+}
+.mdi-car-defrost-front::before {
+ content: "\FD3D";
+}
+.mdi-car-defrost-rear::before {
+ content: "\FD3E";
+}
+.mdi-car-door::before {
+ content: "\FB47";
+}
+.mdi-car-door-lock::before {
+ content: "\F00C8";
+}
+.mdi-car-electric::before {
+ content: "\FB48";
+}
+.mdi-car-esp::before {
+ content: "\FC25";
+}
+.mdi-car-estate::before {
+ content: "\F7A7";
+}
+.mdi-car-hatchback::before {
+ content: "\F7A8";
+}
+.mdi-car-info::before {
+ content: "\F01E9";
+}
+.mdi-car-key::before {
+ content: "\FB49";
+}
+.mdi-car-light-dimmed::before {
+ content: "\FC26";
+}
+.mdi-car-light-fog::before {
+ content: "\FC27";
+}
+.mdi-car-light-high::before {
+ content: "\FC28";
+}
+.mdi-car-limousine::before {
+ content: "\F8CC";
+}
+.mdi-car-multiple::before {
+ content: "\FB4A";
+}
+.mdi-car-off::before {
+ content: "\FDFF";
+}
+.mdi-car-parking-lights::before {
+ content: "\FD3F";
+}
+.mdi-car-pickup::before {
+ content: "\F7A9";
+}
+.mdi-car-seat::before {
+ content: "\FFC4";
+}
+.mdi-car-seat-cooler::before {
+ content: "\FFC5";
+}
+.mdi-car-seat-heater::before {
+ content: "\FFC6";
+}
+.mdi-car-shift-pattern::before {
+ content: "\FF5D";
+}
+.mdi-car-side::before {
+ content: "\F7AA";
+}
+.mdi-car-sports::before {
+ content: "\F7AB";
+}
+.mdi-car-tire-alert::before {
+ content: "\FC29";
+}
+.mdi-car-traction-control::before {
+ content: "\FD40";
+}
+.mdi-car-turbocharger::before {
+ content: "\F003C";
+}
+.mdi-car-wash::before {
+ content: "\F10E";
+}
+.mdi-car-windshield::before {
+ content: "\F003D";
+}
+.mdi-car-windshield-outline::before {
+ content: "\F003E";
+}
+.mdi-caravan::before {
+ content: "\F7AC";
+}
+.mdi-card::before {
+ content: "\FB4B";
+}
+.mdi-card-bulleted::before {
+ content: "\FB4C";
+}
+.mdi-card-bulleted-off::before {
+ content: "\FB4D";
+}
+.mdi-card-bulleted-off-outline::before {
+ content: "\FB4E";
+}
+.mdi-card-bulleted-outline::before {
+ content: "\FB4F";
+}
+.mdi-card-bulleted-settings::before {
+ content: "\FB50";
+}
+.mdi-card-bulleted-settings-outline::before {
+ content: "\FB51";
+}
+.mdi-card-outline::before {
+ content: "\FB52";
+}
+.mdi-card-plus::before {
+ content: "\F022A";
+}
+.mdi-card-plus-outline::before {
+ content: "\F022B";
+}
+.mdi-card-search::before {
+ content: "\F009F";
+}
+.mdi-card-search-outline::before {
+ content: "\F00A0";
+}
+.mdi-card-text::before {
+ content: "\FB53";
+}
+.mdi-card-text-outline::before {
+ content: "\FB54";
+}
+.mdi-cards::before {
+ content: "\F638";
+}
+.mdi-cards-club::before {
+ content: "\F8CD";
+}
+.mdi-cards-diamond::before {
+ content: "\F8CE";
+}
+.mdi-cards-diamond-outline::before {
+ content: "\F003F";
+}
+.mdi-cards-heart::before {
+ content: "\F8CF";
+}
+.mdi-cards-outline::before {
+ content: "\F639";
+}
+.mdi-cards-playing-outline::before {
+ content: "\F63A";
+}
+.mdi-cards-spade::before {
+ content: "\F8D0";
+}
+.mdi-cards-variant::before {
+ content: "\F6C6";
+}
+.mdi-carrot::before {
+ content: "\F10F";
+}
+.mdi-cart::before {
+ content: "\F110";
+}
+.mdi-cart-arrow-down::before {
+ content: "\FD42";
+}
+.mdi-cart-arrow-right::before {
+ content: "\FC2A";
+}
+.mdi-cart-arrow-up::before {
+ content: "\FD43";
+}
+.mdi-cart-minus::before {
+ content: "\FD44";
+}
+.mdi-cart-off::before {
+ content: "\F66B";
+}
+.mdi-cart-outline::before {
+ content: "\F111";
+}
+.mdi-cart-plus::before {
+ content: "\F112";
+}
+.mdi-cart-remove::before {
+ content: "\FD45";
+}
+.mdi-case-sensitive-alt::before {
+ content: "\F113";
+}
+.mdi-cash::before {
+ content: "\F114";
+}
+.mdi-cash-100::before {
+ content: "\F115";
+}
+.mdi-cash-marker::before {
+ content: "\FD94";
+}
+.mdi-cash-minus::before {
+ content: "\F028B";
+}
+.mdi-cash-multiple::before {
+ content: "\F116";
+}
+.mdi-cash-plus::before {
+ content: "\F028C";
+}
+.mdi-cash-refund::before {
+ content: "\FA9B";
+}
+.mdi-cash-register::before {
+ content: "\FCD0";
+}
+.mdi-cash-remove::before {
+ content: "\F028D";
+}
+.mdi-cash-usd::before {
+ content: "\F01A1";
+}
+.mdi-cash-usd-outline::before {
+ content: "\F117";
+}
+.mdi-cassette::before {
+ content: "\F9D3";
+}
+.mdi-cast::before {
+ content: "\F118";
+}
+.mdi-cast-audio::before {
+ content: "\F0040";
+}
+.mdi-cast-connected::before {
+ content: "\F119";
+}
+.mdi-cast-education::before {
+ content: "\FE6D";
+}
+.mdi-cast-off::before {
+ content: "\F789";
+}
+.mdi-castle::before {
+ content: "\F11A";
+}
+.mdi-cat::before {
+ content: "\F11B";
+}
+.mdi-cctv::before {
+ content: "\F7AD";
+}
+.mdi-ceiling-light::before {
+ content: "\F768";
+}
+.mdi-cellphone::before {
+ content: "\F11C";
+}
+.mdi-cellphone-android::before {
+ content: "\F11D";
+}
+.mdi-cellphone-arrow-down::before {
+ content: "\F9D4";
+}
+.mdi-cellphone-basic::before {
+ content: "\F11E";
+}
+.mdi-cellphone-dock::before {
+ content: "\F11F";
+}
+.mdi-cellphone-erase::before {
+ content: "\F94C";
+}
+.mdi-cellphone-information::before {
+ content: "\FF5E";
+}
+.mdi-cellphone-iphone::before {
+ content: "\F120";
+}
+.mdi-cellphone-key::before {
+ content: "\F94D";
+}
+.mdi-cellphone-link::before {
+ content: "\F121";
+}
+.mdi-cellphone-link-off::before {
+ content: "\F122";
+}
+.mdi-cellphone-lock::before {
+ content: "\F94E";
+}
+.mdi-cellphone-message::before {
+ content: "\F8D2";
+}
+.mdi-cellphone-message-off::before {
+ content: "\F00FD";
+}
+.mdi-cellphone-nfc::before {
+ content: "\FEAD";
+}
+.mdi-cellphone-nfc-off::before {
+ content: "\F0303";
+}
+.mdi-cellphone-off::before {
+ content: "\F94F";
+}
+.mdi-cellphone-play::before {
+ content: "\F0041";
+}
+.mdi-cellphone-screenshot::before {
+ content: "\FA34";
+}
+.mdi-cellphone-settings::before {
+ content: "\F123";
+}
+.mdi-cellphone-settings-variant::before {
+ content: "\F950";
+}
+.mdi-cellphone-sound::before {
+ content: "\F951";
+}
+.mdi-cellphone-text::before {
+ content: "\F8D1";
+}
+.mdi-cellphone-wireless::before {
+ content: "\F814";
+}
+.mdi-celtic-cross::before {
+ content: "\FCD1";
+}
+.mdi-centos::before {
+ content: "\F0145";
+}
+.mdi-certificate::before {
+ content: "\F124";
+}
+.mdi-certificate-outline::before {
+ content: "\F01B3";
+}
+.mdi-chair-rolling::before {
+ content: "\FFBA";
+}
+.mdi-chair-school::before {
+ content: "\F125";
+}
+.mdi-charity::before {
+ content: "\FC2B";
+}
+.mdi-chart-arc::before {
+ content: "\F126";
+}
+.mdi-chart-areaspline::before {
+ content: "\F127";
+}
+.mdi-chart-areaspline-variant::before {
+ content: "\FEAE";
+}
+.mdi-chart-bar::before {
+ content: "\F128";
+}
+.mdi-chart-bar-stacked::before {
+ content: "\F769";
+}
+.mdi-chart-bell-curve::before {
+ content: "\FC2C";
+}
+.mdi-chart-bell-curve-cumulative::before {
+ content: "\FFC7";
+}
+.mdi-chart-bubble::before {
+ content: "\F5E3";
+}
+.mdi-chart-donut::before {
+ content: "\F7AE";
+}
+.mdi-chart-donut-variant::before {
+ content: "\F7AF";
+}
+.mdi-chart-gantt::before {
+ content: "\F66C";
+}
+.mdi-chart-histogram::before {
+ content: "\F129";
+}
+.mdi-chart-line::before {
+ content: "\F12A";
+}
+.mdi-chart-line-stacked::before {
+ content: "\F76A";
+}
+.mdi-chart-line-variant::before {
+ content: "\F7B0";
+}
+.mdi-chart-multiline::before {
+ content: "\F8D3";
+}
+.mdi-chart-multiple::before {
+ content: "\F023E";
+}
+.mdi-chart-pie::before {
+ content: "\F12B";
+}
+.mdi-chart-ppf::before {
+ content: "\F03AB";
+}
+.mdi-chart-scatter-plot::before {
+ content: "\FEAF";
+}
+.mdi-chart-scatter-plot-hexbin::before {
+ content: "\F66D";
+}
+.mdi-chart-snakey::before {
+ content: "\F020A";
+}
+.mdi-chart-snakey-variant::before {
+ content: "\F020B";
+}
+.mdi-chart-timeline::before {
+ content: "\F66E";
+}
+.mdi-chart-timeline-variant::before {
+ content: "\FEB0";
+}
+.mdi-chart-tree::before {
+ content: "\FEB1";
+}
+.mdi-chat::before {
+ content: "\FB55";
+}
+.mdi-chat-alert::before {
+ content: "\FB56";
+}
+.mdi-chat-alert-outline::before {
+ content: "\F02F4";
+}
+.mdi-chat-outline::before {
+ content: "\FEFB";
+}
+.mdi-chat-processing::before {
+ content: "\FB57";
+}
+.mdi-chat-processing-outline::before {
+ content: "\F02F5";
+}
+.mdi-chat-sleep::before {
+ content: "\F02FC";
+}
+.mdi-chat-sleep-outline::before {
+ content: "\F02FD";
+}
+.mdi-check::before {
+ content: "\F12C";
+}
+.mdi-check-all::before {
+ content: "\F12D";
+}
+.mdi-check-bold::before {
+ content: "\FE6E";
+}
+.mdi-check-box-multiple-outline::before {
+ content: "\FC2D";
+}
+.mdi-check-box-outline::before {
+ content: "\FC2E";
+}
+.mdi-check-circle::before {
+ content: "\F5E0";
+}
+.mdi-check-circle-outline::before {
+ content: "\F5E1";
+}
+.mdi-check-decagram::before {
+ content: "\F790";
+}
+.mdi-check-network::before {
+ content: "\FC2F";
+}
+.mdi-check-network-outline::before {
+ content: "\FC30";
+}
+.mdi-check-outline::before {
+ content: "\F854";
+}
+.mdi-check-underline::before {
+ content: "\FE70";
+}
+.mdi-check-underline-circle::before {
+ content: "\FE71";
+}
+.mdi-check-underline-circle-outline::before {
+ content: "\FE72";
+}
+.mdi-checkbook::before {
+ content: "\FA9C";
+}
+.mdi-checkbox-blank::before {
+ content: "\F12E";
+}
+.mdi-checkbox-blank-circle::before {
+ content: "\F12F";
+}
+.mdi-checkbox-blank-circle-outline::before {
+ content: "\F130";
+}
+.mdi-checkbox-blank-off::before {
+ content: "\F0317";
+}
+.mdi-checkbox-blank-off-outline::before {
+ content: "\F0318";
+}
+.mdi-checkbox-blank-outline::before {
+ content: "\F131";
+}
+.mdi-checkbox-intermediate::before {
+ content: "\F855";
+}
+.mdi-checkbox-marked::before {
+ content: "\F132";
+}
+.mdi-checkbox-marked-circle::before {
+ content: "\F133";
+}
+.mdi-checkbox-marked-circle-outline::before {
+ content: "\F134";
+}
+.mdi-checkbox-marked-outline::before {
+ content: "\F135";
+}
+.mdi-checkbox-multiple-blank::before {
+ content: "\F136";
+}
+.mdi-checkbox-multiple-blank-circle::before {
+ content: "\F63B";
+}
+.mdi-checkbox-multiple-blank-circle-outline::before {
+ content: "\F63C";
+}
+.mdi-checkbox-multiple-blank-outline::before {
+ content: "\F137";
+}
+.mdi-checkbox-multiple-marked::before {
+ content: "\F138";
+}
+.mdi-checkbox-multiple-marked-circle::before {
+ content: "\F63D";
+}
+.mdi-checkbox-multiple-marked-circle-outline::before {
+ content: "\F63E";
+}
+.mdi-checkbox-multiple-marked-outline::before {
+ content: "\F139";
+}
+.mdi-checkerboard::before {
+ content: "\F13A";
+}
+.mdi-checkerboard-minus::before {
+ content: "\F022D";
+}
+.mdi-checkerboard-plus::before {
+ content: "\F022C";
+}
+.mdi-checkerboard-remove::before {
+ content: "\F022E";
+}
+.mdi-cheese::before {
+ content: "\F02E4";
+}
+.mdi-chef-hat::before {
+ content: "\FB58";
+}
+.mdi-chemical-weapon::before {
+ content: "\F13B";
+}
+.mdi-chess-bishop::before {
+ content: "\F85B";
+}
+.mdi-chess-king::before {
+ content: "\F856";
+}
+.mdi-chess-knight::before {
+ content: "\F857";
+}
+.mdi-chess-pawn::before {
+ content: "\F858";
+}
+.mdi-chess-queen::before {
+ content: "\F859";
+}
+.mdi-chess-rook::before {
+ content: "\F85A";
+}
+.mdi-chevron-double-down::before {
+ content: "\F13C";
+}
+.mdi-chevron-double-left::before {
+ content: "\F13D";
+}
+.mdi-chevron-double-right::before {
+ content: "\F13E";
+}
+.mdi-chevron-double-up::before {
+ content: "\F13F";
+}
+.mdi-chevron-down::before {
+ content: "\F140";
+}
+.mdi-chevron-down-box::before {
+ content: "\F9D5";
+}
+.mdi-chevron-down-box-outline::before {
+ content: "\F9D6";
+}
+.mdi-chevron-down-circle::before {
+ content: "\FB0B";
+}
+.mdi-chevron-down-circle-outline::before {
+ content: "\FB0C";
+}
+.mdi-chevron-left::before {
+ content: "\F141";
+}
+.mdi-chevron-left-box::before {
+ content: "\F9D7";
+}
+.mdi-chevron-left-box-outline::before {
+ content: "\F9D8";
+}
+.mdi-chevron-left-circle::before {
+ content: "\FB0D";
+}
+.mdi-chevron-left-circle-outline::before {
+ content: "\FB0E";
+}
+.mdi-chevron-right::before {
+ content: "\F142";
+}
+.mdi-chevron-right-box::before {
+ content: "\F9D9";
+}
+.mdi-chevron-right-box-outline::before {
+ content: "\F9DA";
+}
+.mdi-chevron-right-circle::before {
+ content: "\FB0F";
+}
+.mdi-chevron-right-circle-outline::before {
+ content: "\FB10";
+}
+.mdi-chevron-triple-down::before {
+ content: "\FD95";
+}
+.mdi-chevron-triple-left::before {
+ content: "\FD96";
+}
+.mdi-chevron-triple-right::before {
+ content: "\FD97";
+}
+.mdi-chevron-triple-up::before {
+ content: "\FD98";
+}
+.mdi-chevron-up::before {
+ content: "\F143";
+}
+.mdi-chevron-up-box::before {
+ content: "\F9DB";
+}
+.mdi-chevron-up-box-outline::before {
+ content: "\F9DC";
+}
+.mdi-chevron-up-circle::before {
+ content: "\FB11";
+}
+.mdi-chevron-up-circle-outline::before {
+ content: "\FB12";
+}
+.mdi-chili-hot::before {
+ content: "\F7B1";
+}
+.mdi-chili-medium::before {
+ content: "\F7B2";
+}
+.mdi-chili-mild::before {
+ content: "\F7B3";
+}
+.mdi-chip::before {
+ content: "\F61A";
+}
+.mdi-christianity::before {
+ content: "\F952";
+}
+.mdi-christianity-outline::before {
+ content: "\FCD2";
+}
+.mdi-church::before {
+ content: "\F144";
+}
+.mdi-cigar::before {
+ content: "\F01B4";
+}
+.mdi-circle::before {
+ content: "\F764";
+}
+.mdi-circle-double::before {
+ content: "\FEB2";
+}
+.mdi-circle-edit-outline::before {
+ content: "\F8D4";
+}
+.mdi-circle-expand::before {
+ content: "\FEB3";
+}
+.mdi-circle-medium::before {
+ content: "\F9DD";
+}
+.mdi-circle-off-outline::before {
+ content: "\F00FE";
+}
+.mdi-circle-outline::before {
+ content: "\F765";
+}
+.mdi-circle-slice-1::before {
+ content: "\FA9D";
+}
+.mdi-circle-slice-2::before {
+ content: "\FA9E";
+}
+.mdi-circle-slice-3::before {
+ content: "\FA9F";
+}
+.mdi-circle-slice-4::before {
+ content: "\FAA0";
+}
+.mdi-circle-slice-5::before {
+ content: "\FAA1";
+}
+.mdi-circle-slice-6::before {
+ content: "\FAA2";
+}
+.mdi-circle-slice-7::before {
+ content: "\FAA3";
+}
+.mdi-circle-slice-8::before {
+ content: "\FAA4";
+}
+.mdi-circle-small::before {
+ content: "\F9DE";
+}
+.mdi-circular-saw::before {
+ content: "\FE73";
+}
+.mdi-cisco-webex::before {
+ content: "\F145";
+}
+.mdi-city::before {
+ content: "\F146";
+}
+.mdi-city-variant::before {
+ content: "\FA35";
+}
+.mdi-city-variant-outline::before {
+ content: "\FA36";
+}
+.mdi-clipboard::before {
+ content: "\F147";
+}
+.mdi-clipboard-account::before {
+ content: "\F148";
+}
+.mdi-clipboard-account-outline::before {
+ content: "\FC31";
+}
+.mdi-clipboard-alert::before {
+ content: "\F149";
+}
+.mdi-clipboard-alert-outline::before {
+ content: "\FCD3";
+}
+.mdi-clipboard-arrow-down::before {
+ content: "\F14A";
+}
+.mdi-clipboard-arrow-down-outline::before {
+ content: "\FC32";
+}
+.mdi-clipboard-arrow-left::before {
+ content: "\F14B";
+}
+.mdi-clipboard-arrow-left-outline::before {
+ content: "\FCD4";
+}
+.mdi-clipboard-arrow-right::before {
+ content: "\FCD5";
+}
+.mdi-clipboard-arrow-right-outline::before {
+ content: "\FCD6";
+}
+.mdi-clipboard-arrow-up::before {
+ content: "\FC33";
+}
+.mdi-clipboard-arrow-up-outline::before {
+ content: "\FC34";
+}
+.mdi-clipboard-check::before {
+ content: "\F14C";
+}
+.mdi-clipboard-check-multiple::before {
+ content: "\F028E";
+}
+.mdi-clipboard-check-multiple-outline::before {
+ content: "\F028F";
+}
+.mdi-clipboard-check-outline::before {
+ content: "\F8A7";
+}
+.mdi-clipboard-file::before {
+ content: "\F0290";
+}
+.mdi-clipboard-file-outline::before {
+ content: "\F0291";
+}
+.mdi-clipboard-flow::before {
+ content: "\F6C7";
+}
+.mdi-clipboard-flow-outline::before {
+ content: "\F0142";
+}
+.mdi-clipboard-list::before {
+ content: "\F00FF";
+}
+.mdi-clipboard-list-outline::before {
+ content: "\F0100";
+}
+.mdi-clipboard-multiple::before {
+ content: "\F0292";
+}
+.mdi-clipboard-multiple-outline::before {
+ content: "\F0293";
+}
+.mdi-clipboard-outline::before {
+ content: "\F14D";
+}
+.mdi-clipboard-play::before {
+ content: "\FC35";
+}
+.mdi-clipboard-play-multiple::before {
+ content: "\F0294";
+}
+.mdi-clipboard-play-multiple-outline::before {
+ content: "\F0295";
+}
+.mdi-clipboard-play-outline::before {
+ content: "\FC36";
+}
+.mdi-clipboard-plus::before {
+ content: "\F750";
+}
+.mdi-clipboard-plus-outline::before {
+ content: "\F034A";
+}
+.mdi-clipboard-pulse::before {
+ content: "\F85C";
+}
+.mdi-clipboard-pulse-outline::before {
+ content: "\F85D";
+}
+.mdi-clipboard-text::before {
+ content: "\F14E";
+}
+.mdi-clipboard-text-multiple::before {
+ content: "\F0296";
+}
+.mdi-clipboard-text-multiple-outline::before {
+ content: "\F0297";
+}
+.mdi-clipboard-text-outline::before {
+ content: "\FA37";
+}
+.mdi-clipboard-text-play::before {
+ content: "\FC37";
+}
+.mdi-clipboard-text-play-outline::before {
+ content: "\FC38";
+}
+.mdi-clippy::before {
+ content: "\F14F";
+}
+.mdi-clock::before {
+ content: "\F953";
+}
+.mdi-clock-alert::before {
+ content: "\F954";
+}
+.mdi-clock-alert-outline::before {
+ content: "\F5CE";
+}
+.mdi-clock-check::before {
+ content: "\FFC8";
+}
+.mdi-clock-check-outline::before {
+ content: "\FFC9";
+}
+.mdi-clock-digital::before {
+ content: "\FEB4";
+}
+.mdi-clock-end::before {
+ content: "\F151";
+}
+.mdi-clock-fast::before {
+ content: "\F152";
+}
+.mdi-clock-in::before {
+ content: "\F153";
+}
+.mdi-clock-out::before {
+ content: "\F154";
+}
+.mdi-clock-outline::before {
+ content: "\F150";
+}
+.mdi-clock-start::before {
+ content: "\F155";
+}
+.mdi-close::before {
+ content: "\F156";
+}
+.mdi-close-box::before {
+ content: "\F157";
+}
+.mdi-close-box-multiple::before {
+ content: "\FC39";
+}
+.mdi-close-box-multiple-outline::before {
+ content: "\FC3A";
+}
+.mdi-close-box-outline::before {
+ content: "\F158";
+}
+.mdi-close-circle::before {
+ content: "\F159";
+}
+.mdi-close-circle-outline::before {
+ content: "\F15A";
+}
+.mdi-close-network::before {
+ content: "\F15B";
+}
+.mdi-close-network-outline::before {
+ content: "\FC3B";
+}
+.mdi-close-octagon::before {
+ content: "\F15C";
+}
+.mdi-close-octagon-outline::before {
+ content: "\F15D";
+}
+.mdi-close-outline::before {
+ content: "\F6C8";
+}
+.mdi-closed-caption::before {
+ content: "\F15E";
+}
+.mdi-closed-caption-outline::before {
+ content: "\FD99";
+}
+.mdi-cloud::before {
+ content: "\F15F";
+}
+.mdi-cloud-alert::before {
+ content: "\F9DF";
+}
+.mdi-cloud-braces::before {
+ content: "\F7B4";
+}
+.mdi-cloud-check::before {
+ content: "\F160";
+}
+.mdi-cloud-check-outline::before {
+ content: "\F02F7";
+}
+.mdi-cloud-circle::before {
+ content: "\F161";
+}
+.mdi-cloud-download::before {
+ content: "\F162";
+}
+.mdi-cloud-download-outline::before {
+ content: "\FB59";
+}
+.mdi-cloud-lock::before {
+ content: "\F021C";
+}
+.mdi-cloud-lock-outline::before {
+ content: "\F021D";
+}
+.mdi-cloud-off-outline::before {
+ content: "\F164";
+}
+.mdi-cloud-outline::before {
+ content: "\F163";
+}
+.mdi-cloud-print::before {
+ content: "\F165";
+}
+.mdi-cloud-print-outline::before {
+ content: "\F166";
+}
+.mdi-cloud-question::before {
+ content: "\FA38";
+}
+.mdi-cloud-search::before {
+ content: "\F955";
+}
+.mdi-cloud-search-outline::before {
+ content: "\F956";
+}
+.mdi-cloud-sync::before {
+ content: "\F63F";
+}
+.mdi-cloud-sync-outline::before {
+ content: "\F0301";
+}
+.mdi-cloud-tags::before {
+ content: "\F7B5";
+}
+.mdi-cloud-upload::before {
+ content: "\F167";
+}
+.mdi-cloud-upload-outline::before {
+ content: "\FB5A";
+}
+.mdi-clover::before {
+ content: "\F815";
+}
+.mdi-coach-lamp::before {
+ content: "\F0042";
+}
+.mdi-coat-rack::before {
+ content: "\F00C9";
+}
+.mdi-code-array::before {
+ content: "\F168";
+}
+.mdi-code-braces::before {
+ content: "\F169";
+}
+.mdi-code-braces-box::before {
+ content: "\F0101";
+}
+.mdi-code-brackets::before {
+ content: "\F16A";
+}
+.mdi-code-equal::before {
+ content: "\F16B";
+}
+.mdi-code-greater-than::before {
+ content: "\F16C";
+}
+.mdi-code-greater-than-or-equal::before {
+ content: "\F16D";
+}
+.mdi-code-less-than::before {
+ content: "\F16E";
+}
+.mdi-code-less-than-or-equal::before {
+ content: "\F16F";
+}
+.mdi-code-not-equal::before {
+ content: "\F170";
+}
+.mdi-code-not-equal-variant::before {
+ content: "\F171";
+}
+.mdi-code-parentheses::before {
+ content: "\F172";
+}
+.mdi-code-parentheses-box::before {
+ content: "\F0102";
+}
+.mdi-code-string::before {
+ content: "\F173";
+}
+.mdi-code-tags::before {
+ content: "\F174";
+}
+.mdi-code-tags-check::before {
+ content: "\F693";
+}
+.mdi-codepen::before {
+ content: "\F175";
+}
+.mdi-coffee::before {
+ content: "\F176";
+}
+.mdi-coffee-maker::before {
+ content: "\F00CA";
+}
+.mdi-coffee-off::before {
+ content: "\FFCA";
+}
+.mdi-coffee-off-outline::before {
+ content: "\FFCB";
+}
+.mdi-coffee-outline::before {
+ content: "\F6C9";
+}
+.mdi-coffee-to-go::before {
+ content: "\F177";
+}
+.mdi-coffee-to-go-outline::before {
+ content: "\F0339";
+}
+.mdi-coffin::before {
+ content: "\FB5B";
+}
+.mdi-cog-clockwise::before {
+ content: "\F0208";
+}
+.mdi-cog-counterclockwise::before {
+ content: "\F0209";
+}
+.mdi-cogs::before {
+ content: "\F8D5";
+}
+.mdi-coin::before {
+ content: "\F0196";
+}
+.mdi-coin-outline::before {
+ content: "\F178";
+}
+.mdi-coins::before {
+ content: "\F694";
+}
+.mdi-collage::before {
+ content: "\F640";
+}
+.mdi-collapse-all::before {
+ content: "\FAA5";
+}
+.mdi-collapse-all-outline::before {
+ content: "\FAA6";
+}
+.mdi-color-helper::before {
+ content: "\F179";
+}
+.mdi-comma::before {
+ content: "\FE74";
+}
+.mdi-comma-box::before {
+ content: "\FE75";
+}
+.mdi-comma-box-outline::before {
+ content: "\FE76";
+}
+.mdi-comma-circle::before {
+ content: "\FE77";
+}
+.mdi-comma-circle-outline::before {
+ content: "\FE78";
+}
+.mdi-comment::before {
+ content: "\F17A";
+}
+.mdi-comment-account::before {
+ content: "\F17B";
+}
+.mdi-comment-account-outline::before {
+ content: "\F17C";
+}
+.mdi-comment-alert::before {
+ content: "\F17D";
+}
+.mdi-comment-alert-outline::before {
+ content: "\F17E";
+}
+.mdi-comment-arrow-left::before {
+ content: "\F9E0";
+}
+.mdi-comment-arrow-left-outline::before {
+ content: "\F9E1";
+}
+.mdi-comment-arrow-right::before {
+ content: "\F9E2";
+}
+.mdi-comment-arrow-right-outline::before {
+ content: "\F9E3";
+}
+.mdi-comment-check::before {
+ content: "\F17F";
+}
+.mdi-comment-check-outline::before {
+ content: "\F180";
+}
+.mdi-comment-edit::before {
+ content: "\F01EA";
+}
+.mdi-comment-edit-outline::before {
+ content: "\F02EF";
+}
+.mdi-comment-eye::before {
+ content: "\FA39";
+}
+.mdi-comment-eye-outline::before {
+ content: "\FA3A";
+}
+.mdi-comment-multiple::before {
+ content: "\F85E";
+}
+.mdi-comment-multiple-outline::before {
+ content: "\F181";
+}
+.mdi-comment-outline::before {
+ content: "\F182";
+}
+.mdi-comment-plus::before {
+ content: "\F9E4";
+}
+.mdi-comment-plus-outline::before {
+ content: "\F183";
+}
+.mdi-comment-processing::before {
+ content: "\F184";
+}
+.mdi-comment-processing-outline::before {
+ content: "\F185";
+}
+.mdi-comment-question::before {
+ content: "\F816";
+}
+.mdi-comment-question-outline::before {
+ content: "\F186";
+}
+.mdi-comment-quote::before {
+ content: "\F0043";
+}
+.mdi-comment-quote-outline::before {
+ content: "\F0044";
+}
+.mdi-comment-remove::before {
+ content: "\F5DE";
+}
+.mdi-comment-remove-outline::before {
+ content: "\F187";
+}
+.mdi-comment-search::before {
+ content: "\FA3B";
+}
+.mdi-comment-search-outline::before {
+ content: "\FA3C";
+}
+.mdi-comment-text::before {
+ content: "\F188";
+}
+.mdi-comment-text-multiple::before {
+ content: "\F85F";
+}
+.mdi-comment-text-multiple-outline::before {
+ content: "\F860";
+}
+.mdi-comment-text-outline::before {
+ content: "\F189";
+}
+.mdi-compare::before {
+ content: "\F18A";
+}
+.mdi-compass::before {
+ content: "\F18B";
+}
+.mdi-compass-off::before {
+ content: "\FB5C";
+}
+.mdi-compass-off-outline::before {
+ content: "\FB5D";
+}
+.mdi-compass-outline::before {
+ content: "\F18C";
+}
+.mdi-compass-rose::before {
+ content: "\F03AD";
+}
+.mdi-concourse-ci::before {
+ content: "\F00CB";
+}
+.mdi-console::before {
+ content: "\F18D";
+}
+.mdi-console-line::before {
+ content: "\F7B6";
+}
+.mdi-console-network::before {
+ content: "\F8A8";
+}
+.mdi-console-network-outline::before {
+ content: "\FC3C";
+}
+.mdi-consolidate::before {
+ content: "\F0103";
+}
+.mdi-contact-mail::before {
+ content: "\F18E";
+}
+.mdi-contact-mail-outline::before {
+ content: "\FEB5";
+}
+.mdi-contact-phone::before {
+ content: "\FEB6";
+}
+.mdi-contact-phone-outline::before {
+ content: "\FEB7";
+}
+.mdi-contactless-payment::before {
+ content: "\FD46";
+}
+.mdi-contacts::before {
+ content: "\F6CA";
+}
+.mdi-contain::before {
+ content: "\FA3D";
+}
+.mdi-contain-end::before {
+ content: "\FA3E";
+}
+.mdi-contain-start::before {
+ content: "\FA3F";
+}
+.mdi-content-copy::before {
+ content: "\F18F";
+}
+.mdi-content-cut::before {
+ content: "\F190";
+}
+.mdi-content-duplicate::before {
+ content: "\F191";
+}
+.mdi-content-paste::before {
+ content: "\F192";
+}
+.mdi-content-save::before {
+ content: "\F193";
+}
+.mdi-content-save-alert::before {
+ content: "\FF5F";
+}
+.mdi-content-save-alert-outline::before {
+ content: "\FF60";
+}
+.mdi-content-save-all::before {
+ content: "\F194";
+}
+.mdi-content-save-all-outline::before {
+ content: "\FF61";
+}
+.mdi-content-save-edit::before {
+ content: "\FCD7";
+}
+.mdi-content-save-edit-outline::before {
+ content: "\FCD8";
+}
+.mdi-content-save-move::before {
+ content: "\FE79";
+}
+.mdi-content-save-move-outline::before {
+ content: "\FE7A";
+}
+.mdi-content-save-outline::before {
+ content: "\F817";
+}
+.mdi-content-save-settings::before {
+ content: "\F61B";
+}
+.mdi-content-save-settings-outline::before {
+ content: "\FB13";
+}
+.mdi-contrast::before {
+ content: "\F195";
+}
+.mdi-contrast-box::before {
+ content: "\F196";
+}
+.mdi-contrast-circle::before {
+ content: "\F197";
+}
+.mdi-controller-classic::before {
+ content: "\FB5E";
+}
+.mdi-controller-classic-outline::before {
+ content: "\FB5F";
+}
+.mdi-cookie::before {
+ content: "\F198";
+}
+.mdi-coolant-temperature::before {
+ content: "\F3C8";
+}
+.mdi-copyright::before {
+ content: "\F5E6";
+}
+.mdi-cordova::before {
+ content: "\F957";
+}
+.mdi-corn::before {
+ content: "\F7B7";
+}
+.mdi-counter::before {
+ content: "\F199";
+}
+.mdi-cow::before {
+ content: "\F19A";
+}
+.mdi-cowboy::before {
+ content: "\FEB8";
+}
+.mdi-cpu-32-bit::before {
+ content: "\FEFC";
+}
+.mdi-cpu-64-bit::before {
+ content: "\FEFD";
+}
+.mdi-crane::before {
+ content: "\F861";
+}
+.mdi-creation::before {
+ content: "\F1C9";
+}
+.mdi-creative-commons::before {
+ content: "\FD47";
+}
+.mdi-credit-card::before {
+ content: "\F0010";
+}
+.mdi-credit-card-clock::before {
+ content: "\FEFE";
+}
+.mdi-credit-card-clock-outline::before {
+ content: "\FFBC";
+}
+.mdi-credit-card-marker::before {
+ content: "\F6A7";
+}
+.mdi-credit-card-marker-outline::before {
+ content: "\FD9A";
+}
+.mdi-credit-card-minus::before {
+ content: "\FFCC";
+}
+.mdi-credit-card-minus-outline::before {
+ content: "\FFCD";
+}
+.mdi-credit-card-multiple::before {
+ content: "\F0011";
+}
+.mdi-credit-card-multiple-outline::before {
+ content: "\F19C";
+}
+.mdi-credit-card-off::before {
+ content: "\F0012";
+}
+.mdi-credit-card-off-outline::before {
+ content: "\F5E4";
+}
+.mdi-credit-card-outline::before {
+ content: "\F19B";
+}
+.mdi-credit-card-plus::before {
+ content: "\F0013";
+}
+.mdi-credit-card-plus-outline::before {
+ content: "\F675";
+}
+.mdi-credit-card-refund::before {
+ content: "\F0014";
+}
+.mdi-credit-card-refund-outline::before {
+ content: "\FAA7";
+}
+.mdi-credit-card-remove::before {
+ content: "\FFCE";
+}
+.mdi-credit-card-remove-outline::before {
+ content: "\FFCF";
+}
+.mdi-credit-card-scan::before {
+ content: "\F0015";
+}
+.mdi-credit-card-scan-outline::before {
+ content: "\F19D";
+}
+.mdi-credit-card-settings::before {
+ content: "\F0016";
+}
+.mdi-credit-card-settings-outline::before {
+ content: "\F8D6";
+}
+.mdi-credit-card-wireless::before {
+ content: "\F801";
+}
+.mdi-credit-card-wireless-outline::before {
+ content: "\FD48";
+}
+.mdi-cricket::before {
+ content: "\FD49";
+}
+.mdi-crop::before {
+ content: "\F19E";
+}
+.mdi-crop-free::before {
+ content: "\F19F";
+}
+.mdi-crop-landscape::before {
+ content: "\F1A0";
+}
+.mdi-crop-portrait::before {
+ content: "\F1A1";
+}
+.mdi-crop-rotate::before {
+ content: "\F695";
+}
+.mdi-crop-square::before {
+ content: "\F1A2";
+}
+.mdi-crosshairs::before {
+ content: "\F1A3";
+}
+.mdi-crosshairs-gps::before {
+ content: "\F1A4";
+}
+.mdi-crosshairs-off::before {
+ content: "\FF62";
+}
+.mdi-crosshairs-question::before {
+ content: "\F0161";
+}
+.mdi-crown::before {
+ content: "\F1A5";
+}
+.mdi-crown-outline::before {
+ content: "\F01FB";
+}
+.mdi-cryengine::before {
+ content: "\F958";
+}
+.mdi-crystal-ball::before {
+ content: "\FB14";
+}
+.mdi-cube::before {
+ content: "\F1A6";
+}
+.mdi-cube-outline::before {
+ content: "\F1A7";
+}
+.mdi-cube-scan::before {
+ content: "\FB60";
+}
+.mdi-cube-send::before {
+ content: "\F1A8";
+}
+.mdi-cube-unfolded::before {
+ content: "\F1A9";
+}
+.mdi-cup::before {
+ content: "\F1AA";
+}
+.mdi-cup-off::before {
+ content: "\F5E5";
+}
+.mdi-cup-off-outline::before {
+ content: "\F03A8";
+}
+.mdi-cup-outline::before {
+ content: "\F033A";
+}
+.mdi-cup-water::before {
+ content: "\F1AB";
+}
+.mdi-cupboard::before {
+ content: "\FF63";
+}
+.mdi-cupboard-outline::before {
+ content: "\FF64";
+}
+.mdi-cupcake::before {
+ content: "\F959";
+}
+.mdi-curling::before {
+ content: "\F862";
+}
+.mdi-currency-bdt::before {
+ content: "\F863";
+}
+.mdi-currency-brl::before {
+ content: "\FB61";
+}
+.mdi-currency-btc::before {
+ content: "\F1AC";
+}
+.mdi-currency-cny::before {
+ content: "\F7B9";
+}
+.mdi-currency-eth::before {
+ content: "\F7BA";
+}
+.mdi-currency-eur::before {
+ content: "\F1AD";
+}
+.mdi-currency-eur-off::before {
+ content: "\F0340";
+}
+.mdi-currency-gbp::before {
+ content: "\F1AE";
+}
+.mdi-currency-ils::before {
+ content: "\FC3D";
+}
+.mdi-currency-inr::before {
+ content: "\F1AF";
+}
+.mdi-currency-jpy::before {
+ content: "\F7BB";
+}
+.mdi-currency-krw::before {
+ content: "\F7BC";
+}
+.mdi-currency-kzt::before {
+ content: "\F864";
+}
+.mdi-currency-ngn::before {
+ content: "\F1B0";
+}
+.mdi-currency-php::before {
+ content: "\F9E5";
+}
+.mdi-currency-rial::before {
+ content: "\FEB9";
+}
+.mdi-currency-rub::before {
+ content: "\F1B1";
+}
+.mdi-currency-sign::before {
+ content: "\F7BD";
+}
+.mdi-currency-try::before {
+ content: "\F1B2";
+}
+.mdi-currency-twd::before {
+ content: "\F7BE";
+}
+.mdi-currency-usd::before {
+ content: "\F1B3";
+}
+.mdi-currency-usd-off::before {
+ content: "\F679";
+}
+.mdi-current-ac::before {
+ content: "\F95A";
+}
+.mdi-current-dc::before {
+ content: "\F95B";
+}
+.mdi-cursor-default::before {
+ content: "\F1B4";
+}
+.mdi-cursor-default-click::before {
+ content: "\FCD9";
+}
+.mdi-cursor-default-click-outline::before {
+ content: "\FCDA";
+}
+.mdi-cursor-default-gesture::before {
+ content: "\F0152";
+}
+.mdi-cursor-default-gesture-outline::before {
+ content: "\F0153";
+}
+.mdi-cursor-default-outline::before {
+ content: "\F1B5";
+}
+.mdi-cursor-move::before {
+ content: "\F1B6";
+}
+.mdi-cursor-pointer::before {
+ content: "\F1B7";
+}
+.mdi-cursor-text::before {
+ content: "\F5E7";
+}
+.mdi-database::before {
+ content: "\F1B8";
+}
+.mdi-database-check::before {
+ content: "\FAA8";
+}
+.mdi-database-edit::before {
+ content: "\FB62";
+}
+.mdi-database-export::before {
+ content: "\F95D";
+}
+.mdi-database-import::before {
+ content: "\F95C";
+}
+.mdi-database-lock::before {
+ content: "\FAA9";
+}
+.mdi-database-marker::before {
+ content: "\F0321";
+}
+.mdi-database-minus::before {
+ content: "\F1B9";
+}
+.mdi-database-plus::before {
+ content: "\F1BA";
+}
+.mdi-database-refresh::before {
+ content: "\FCDB";
+}
+.mdi-database-remove::before {
+ content: "\FCDC";
+}
+.mdi-database-search::before {
+ content: "\F865";
+}
+.mdi-database-settings::before {
+ content: "\FCDD";
+}
+.mdi-death-star::before {
+ content: "\F8D7";
+}
+.mdi-death-star-variant::before {
+ content: "\F8D8";
+}
+.mdi-deathly-hallows::before {
+ content: "\FB63";
+}
+.mdi-debian::before {
+ content: "\F8D9";
+}
+.mdi-debug-step-into::before {
+ content: "\F1BB";
+}
+.mdi-debug-step-out::before {
+ content: "\F1BC";
+}
+.mdi-debug-step-over::before {
+ content: "\F1BD";
+}
+.mdi-decagram::before {
+ content: "\F76B";
+}
+.mdi-decagram-outline::before {
+ content: "\F76C";
+}
+.mdi-decimal::before {
+ content: "\F00CC";
+}
+.mdi-decimal-comma::before {
+ content: "\F00CD";
+}
+.mdi-decimal-comma-decrease::before {
+ content: "\F00CE";
+}
+.mdi-decimal-comma-increase::before {
+ content: "\F00CF";
+}
+.mdi-decimal-decrease::before {
+ content: "\F1BE";
+}
+.mdi-decimal-increase::before {
+ content: "\F1BF";
+}
+.mdi-delete::before {
+ content: "\F1C0";
+}
+.mdi-delete-alert::before {
+ content: "\F00D0";
+}
+.mdi-delete-alert-outline::before {
+ content: "\F00D1";
+}
+.mdi-delete-circle::before {
+ content: "\F682";
+}
+.mdi-delete-circle-outline::before {
+ content: "\FB64";
+}
+.mdi-delete-empty::before {
+ content: "\F6CB";
+}
+.mdi-delete-empty-outline::before {
+ content: "\FEBA";
+}
+.mdi-delete-forever::before {
+ content: "\F5E8";
+}
+.mdi-delete-forever-outline::before {
+ content: "\FB65";
+}
+.mdi-delete-off::before {
+ content: "\F00D2";
+}
+.mdi-delete-off-outline::before {
+ content: "\F00D3";
+}
+.mdi-delete-outline::before {
+ content: "\F9E6";
+}
+.mdi-delete-restore::before {
+ content: "\F818";
+}
+.mdi-delete-sweep::before {
+ content: "\F5E9";
+}
+.mdi-delete-sweep-outline::before {
+ content: "\FC3E";
+}
+.mdi-delete-variant::before {
+ content: "\F1C1";
+}
+.mdi-delta::before {
+ content: "\F1C2";
+}
+.mdi-desk::before {
+ content: "\F0264";
+}
+.mdi-desk-lamp::before {
+ content: "\F95E";
+}
+.mdi-deskphone::before {
+ content: "\F1C3";
+}
+.mdi-desktop-classic::before {
+ content: "\F7BF";
+}
+.mdi-desktop-mac::before {
+ content: "\F1C4";
+}
+.mdi-desktop-mac-dashboard::before {
+ content: "\F9E7";
+}
+.mdi-desktop-tower::before {
+ content: "\F1C5";
+}
+.mdi-desktop-tower-monitor::before {
+ content: "\FAAA";
+}
+.mdi-details::before {
+ content: "\F1C6";
+}
+.mdi-dev-to::before {
+ content: "\FD4A";
+}
+.mdi-developer-board::before {
+ content: "\F696";
+}
+.mdi-deviantart::before {
+ content: "\F1C7";
+}
+.mdi-devices::before {
+ content: "\FFD0";
+}
+.mdi-diabetes::before {
+ content: "\F0151";
+}
+.mdi-dialpad::before {
+ content: "\F61C";
+}
+.mdi-diameter::before {
+ content: "\FC3F";
+}
+.mdi-diameter-outline::before {
+ content: "\FC40";
+}
+.mdi-diameter-variant::before {
+ content: "\FC41";
+}
+.mdi-diamond::before {
+ content: "\FB66";
+}
+.mdi-diamond-outline::before {
+ content: "\FB67";
+}
+.mdi-diamond-stone::before {
+ content: "\F1C8";
+}
+.mdi-dice-1::before {
+ content: "\F1CA";
+}
+.mdi-dice-1-outline::before {
+ content: "\F0175";
+}
+.mdi-dice-2::before {
+ content: "\F1CB";
+}
+.mdi-dice-2-outline::before {
+ content: "\F0176";
+}
+.mdi-dice-3::before {
+ content: "\F1CC";
+}
+.mdi-dice-3-outline::before {
+ content: "\F0177";
+}
+.mdi-dice-4::before {
+ content: "\F1CD";
+}
+.mdi-dice-4-outline::before {
+ content: "\F0178";
+}
+.mdi-dice-5::before {
+ content: "\F1CE";
+}
+.mdi-dice-5-outline::before {
+ content: "\F0179";
+}
+.mdi-dice-6::before {
+ content: "\F1CF";
+}
+.mdi-dice-6-outline::before {
+ content: "\F017A";
+}
+.mdi-dice-d10::before {
+ content: "\F017E";
+}
+.mdi-dice-d10-outline::before {
+ content: "\F76E";
+}
+.mdi-dice-d12::before {
+ content: "\F017F";
+}
+.mdi-dice-d12-outline::before {
+ content: "\F866";
+}
+.mdi-dice-d20::before {
+ content: "\F0180";
+}
+.mdi-dice-d20-outline::before {
+ content: "\F5EA";
+}
+.mdi-dice-d4::before {
+ content: "\F017B";
+}
+.mdi-dice-d4-outline::before {
+ content: "\F5EB";
+}
+.mdi-dice-d6::before {
+ content: "\F017C";
+}
+.mdi-dice-d6-outline::before {
+ content: "\F5EC";
+}
+.mdi-dice-d8::before {
+ content: "\F017D";
+}
+.mdi-dice-d8-outline::before {
+ content: "\F5ED";
+}
+.mdi-dice-multiple::before {
+ content: "\F76D";
+}
+.mdi-dice-multiple-outline::before {
+ content: "\F0181";
+}
+.mdi-dictionary::before {
+ content: "\F61D";
+}
+.mdi-digital-ocean::before {
+ content: "\F0262";
+}
+.mdi-dip-switch::before {
+ content: "\F7C0";
+}
+.mdi-directions::before {
+ content: "\F1D0";
+}
+.mdi-directions-fork::before {
+ content: "\F641";
+}
+.mdi-disc::before {
+ content: "\F5EE";
+}
+.mdi-disc-alert::before {
+ content: "\F1D1";
+}
+.mdi-disc-player::before {
+ content: "\F95F";
+}
+.mdi-discord::before {
+ content: "\F66F";
+}
+.mdi-dishwasher::before {
+ content: "\FAAB";
+}
+.mdi-dishwasher-alert::before {
+ content: "\F01E3";
+}
+.mdi-dishwasher-off::before {
+ content: "\F01E4";
+}
+.mdi-disqus::before {
+ content: "\F1D2";
+}
+.mdi-disqus-outline::before {
+ content: "\F1D3";
+}
+.mdi-distribute-horizontal-center::before {
+ content: "\F01F4";
+}
+.mdi-distribute-horizontal-left::before {
+ content: "\F01F3";
+}
+.mdi-distribute-horizontal-right::before {
+ content: "\F01F5";
+}
+.mdi-distribute-vertical-bottom::before {
+ content: "\F01F6";
+}
+.mdi-distribute-vertical-center::before {
+ content: "\F01F7";
+}
+.mdi-distribute-vertical-top::before {
+ content: "\F01F8";
+}
+.mdi-diving-flippers::before {
+ content: "\FD9B";
+}
+.mdi-diving-helmet::before {
+ content: "\FD9C";
+}
+.mdi-diving-scuba::before {
+ content: "\FD9D";
+}
+.mdi-diving-scuba-flag::before {
+ content: "\FD9E";
+}
+.mdi-diving-scuba-tank::before {
+ content: "\FD9F";
+}
+.mdi-diving-scuba-tank-multiple::before {
+ content: "\FDA0";
+}
+.mdi-diving-snorkel::before {
+ content: "\FDA1";
+}
+.mdi-division::before {
+ content: "\F1D4";
+}
+.mdi-division-box::before {
+ content: "\F1D5";
+}
+.mdi-dlna::before {
+ content: "\FA40";
+}
+.mdi-dna::before {
+ content: "\F683";
+}
+.mdi-dns::before {
+ content: "\F1D6";
+}
+.mdi-dns-outline::before {
+ content: "\FB68";
+}
+.mdi-do-not-disturb::before {
+ content: "\F697";
+}
+.mdi-do-not-disturb-off::before {
+ content: "\F698";
+}
+.mdi-dock-bottom::before {
+ content: "\F00D4";
+}
+.mdi-dock-left::before {
+ content: "\F00D5";
+}
+.mdi-dock-right::before {
+ content: "\F00D6";
+}
+.mdi-dock-window::before {
+ content: "\F00D7";
+}
+.mdi-docker::before {
+ content: "\F867";
+}
+.mdi-doctor::before {
+ content: "\FA41";
+}
+.mdi-dog::before {
+ content: "\FA42";
+}
+.mdi-dog-service::before {
+ content: "\FAAC";
+}
+.mdi-dog-side::before {
+ content: "\FA43";
+}
+.mdi-dolby::before {
+ content: "\F6B2";
+}
+.mdi-dolly::before {
+ content: "\FEBB";
+}
+.mdi-domain::before {
+ content: "\F1D7";
+}
+.mdi-domain-off::before {
+ content: "\FD4B";
+}
+.mdi-domain-plus::before {
+ content: "\F00D8";
+}
+.mdi-domain-remove::before {
+ content: "\F00D9";
+}
+.mdi-domino-mask::before {
+ content: "\F0045";
+}
+.mdi-donkey::before {
+ content: "\F7C1";
+}
+.mdi-door::before {
+ content: "\F819";
+}
+.mdi-door-closed::before {
+ content: "\F81A";
+}
+.mdi-door-closed-lock::before {
+ content: "\F00DA";
+}
+.mdi-door-open::before {
+ content: "\F81B";
+}
+.mdi-doorbell::before {
+ content: "\F0311";
+}
+.mdi-doorbell-video::before {
+ content: "\F868";
+}
+.mdi-dot-net::before {
+ content: "\FAAD";
+}
+.mdi-dots-horizontal::before {
+ content: "\F1D8";
+}
+.mdi-dots-horizontal-circle::before {
+ content: "\F7C2";
+}
+.mdi-dots-horizontal-circle-outline::before {
+ content: "\FB69";
+}
+.mdi-dots-vertical::before {
+ content: "\F1D9";
+}
+.mdi-dots-vertical-circle::before {
+ content: "\F7C3";
+}
+.mdi-dots-vertical-circle-outline::before {
+ content: "\FB6A";
+}
+.mdi-douban::before {
+ content: "\F699";
+}
+.mdi-download::before {
+ content: "\F1DA";
+}
+.mdi-download-lock::before {
+ content: "\F034B";
+}
+.mdi-download-lock-outline::before {
+ content: "\F034C";
+}
+.mdi-download-multiple::before {
+ content: "\F9E8";
+}
+.mdi-download-network::before {
+ content: "\F6F3";
+}
+.mdi-download-network-outline::before {
+ content: "\FC42";
+}
+.mdi-download-off::before {
+ content: "\F00DB";
+}
+.mdi-download-off-outline::before {
+ content: "\F00DC";
+}
+.mdi-download-outline::before {
+ content: "\FB6B";
+}
+.mdi-drag::before {
+ content: "\F1DB";
+}
+.mdi-drag-horizontal::before {
+ content: "\F1DC";
+}
+.mdi-drag-horizontal-variant::before {
+ content: "\F031B";
+}
+.mdi-drag-variant::before {
+ content: "\FB6C";
+}
+.mdi-drag-vertical::before {
+ content: "\F1DD";
+}
+.mdi-drag-vertical-variant::before {
+ content: "\F031C";
+}
+.mdi-drama-masks::before {
+ content: "\FCDE";
+}
+.mdi-draw::before {
+ content: "\FF66";
+}
+.mdi-drawing::before {
+ content: "\F1DE";
+}
+.mdi-drawing-box::before {
+ content: "\F1DF";
+}
+.mdi-dresser::before {
+ content: "\FF67";
+}
+.mdi-dresser-outline::before {
+ content: "\FF68";
+}
+.mdi-dribbble::before {
+ content: "\F1E0";
+}
+.mdi-dribbble-box::before {
+ content: "\F1E1";
+}
+.mdi-drone::before {
+ content: "\F1E2";
+}
+.mdi-dropbox::before {
+ content: "\F1E3";
+}
+.mdi-drupal::before {
+ content: "\F1E4";
+}
+.mdi-duck::before {
+ content: "\F1E5";
+}
+.mdi-dumbbell::before {
+ content: "\F1E6";
+}
+.mdi-dump-truck::before {
+ content: "\FC43";
+}
+.mdi-ear-hearing::before {
+ content: "\F7C4";
+}
+.mdi-ear-hearing-off::before {
+ content: "\FA44";
+}
+.mdi-earth::before {
+ content: "\F1E7";
+}
+.mdi-earth-arrow-right::before {
+ content: "\F033C";
+}
+.mdi-earth-box::before {
+ content: "\F6CC";
+}
+.mdi-earth-box-off::before {
+ content: "\F6CD";
+}
+.mdi-earth-off::before {
+ content: "\F1E8";
+}
+.mdi-edge::before {
+ content: "\F1E9";
+}
+.mdi-edge-legacy::before {
+ content: "\F027B";
+}
+.mdi-egg::before {
+ content: "\FAAE";
+}
+.mdi-egg-easter::before {
+ content: "\FAAF";
+}
+.mdi-eight-track::before {
+ content: "\F9E9";
+}
+.mdi-eject::before {
+ content: "\F1EA";
+}
+.mdi-eject-outline::before {
+ content: "\FB6D";
+}
+.mdi-electric-switch::before {
+ content: "\FEBC";
+}
+.mdi-electric-switch-closed::before {
+ content: "\F0104";
+}
+.mdi-electron-framework::before {
+ content: "\F0046";
+}
+.mdi-elephant::before {
+ content: "\F7C5";
+}
+.mdi-elevation-decline::before {
+ content: "\F1EB";
+}
+.mdi-elevation-rise::before {
+ content: "\F1EC";
+}
+.mdi-elevator::before {
+ content: "\F1ED";
+}
+.mdi-elevator-down::before {
+ content: "\F02ED";
+}
+.mdi-elevator-passenger::before {
+ content: "\F03AC";
+}
+.mdi-elevator-up::before {
+ content: "\F02EC";
+}
+.mdi-ellipse::before {
+ content: "\FEBD";
+}
+.mdi-ellipse-outline::before {
+ content: "\FEBE";
+}
+.mdi-email::before {
+ content: "\F1EE";
+}
+.mdi-email-alert::before {
+ content: "\F6CE";
+}
+.mdi-email-alert-outline::before {
+ content: "\FD1E";
+}
+.mdi-email-box::before {
+ content: "\FCDF";
+}
+.mdi-email-check::before {
+ content: "\FAB0";
+}
+.mdi-email-check-outline::before {
+ content: "\FAB1";
+}
+.mdi-email-edit::before {
+ content: "\FF00";
+}
+.mdi-email-edit-outline::before {
+ content: "\FF01";
+}
+.mdi-email-lock::before {
+ content: "\F1F1";
+}
+.mdi-email-mark-as-unread::before {
+ content: "\FB6E";
+}
+.mdi-email-minus::before {
+ content: "\FF02";
+}
+.mdi-email-minus-outline::before {
+ content: "\FF03";
+}
+.mdi-email-multiple::before {
+ content: "\FF04";
+}
+.mdi-email-multiple-outline::before {
+ content: "\FF05";
+}
+.mdi-email-newsletter::before {
+ content: "\FFD1";
+}
+.mdi-email-open::before {
+ content: "\F1EF";
+}
+.mdi-email-open-multiple::before {
+ content: "\FF06";
+}
+.mdi-email-open-multiple-outline::before {
+ content: "\FF07";
+}
+.mdi-email-open-outline::before {
+ content: "\F5EF";
+}
+.mdi-email-outline::before {
+ content: "\F1F0";
+}
+.mdi-email-plus::before {
+ content: "\F9EA";
+}
+.mdi-email-plus-outline::before {
+ content: "\F9EB";
+}
+.mdi-email-receive::before {
+ content: "\F0105";
+}
+.mdi-email-receive-outline::before {
+ content: "\F0106";
+}
+.mdi-email-search::before {
+ content: "\F960";
+}
+.mdi-email-search-outline::before {
+ content: "\F961";
+}
+.mdi-email-send::before {
+ content: "\F0107";
+}
+.mdi-email-send-outline::before {
+ content: "\F0108";
+}
+.mdi-email-sync::before {
+ content: "\F02F2";
+}
+.mdi-email-sync-outline::before {
+ content: "\F02F3";
+}
+.mdi-email-variant::before {
+ content: "\F5F0";
+}
+.mdi-ember::before {
+ content: "\FB15";
+}
+.mdi-emby::before {
+ content: "\F6B3";
+}
+.mdi-emoticon::before {
+ content: "\FC44";
+}
+.mdi-emoticon-angry::before {
+ content: "\FC45";
+}
+.mdi-emoticon-angry-outline::before {
+ content: "\FC46";
+}
+.mdi-emoticon-confused::before {
+ content: "\F0109";
+}
+.mdi-emoticon-confused-outline::before {
+ content: "\F010A";
+}
+.mdi-emoticon-cool::before {
+ content: "\FC47";
+}
+.mdi-emoticon-cool-outline::before {
+ content: "\F1F3";
+}
+.mdi-emoticon-cry::before {
+ content: "\FC48";
+}
+.mdi-emoticon-cry-outline::before {
+ content: "\FC49";
+}
+.mdi-emoticon-dead::before {
+ content: "\FC4A";
+}
+.mdi-emoticon-dead-outline::before {
+ content: "\F69A";
+}
+.mdi-emoticon-devil::before {
+ content: "\FC4B";
+}
+.mdi-emoticon-devil-outline::before {
+ content: "\F1F4";
+}
+.mdi-emoticon-excited::before {
+ content: "\FC4C";
+}
+.mdi-emoticon-excited-outline::before {
+ content: "\F69B";
+}
+.mdi-emoticon-frown::before {
+ content: "\FF69";
+}
+.mdi-emoticon-frown-outline::before {
+ content: "\FF6A";
+}
+.mdi-emoticon-happy::before {
+ content: "\FC4D";
+}
+.mdi-emoticon-happy-outline::before {
+ content: "\F1F5";
+}
+.mdi-emoticon-kiss::before {
+ content: "\FC4E";
+}
+.mdi-emoticon-kiss-outline::before {
+ content: "\FC4F";
+}
+.mdi-emoticon-lol::before {
+ content: "\F023F";
+}
+.mdi-emoticon-lol-outline::before {
+ content: "\F0240";
+}
+.mdi-emoticon-neutral::before {
+ content: "\FC50";
+}
+.mdi-emoticon-neutral-outline::before {
+ content: "\F1F6";
+}
+.mdi-emoticon-outline::before {
+ content: "\F1F2";
+}
+.mdi-emoticon-poop::before {
+ content: "\F1F7";
+}
+.mdi-emoticon-poop-outline::before {
+ content: "\FC51";
+}
+.mdi-emoticon-sad::before {
+ content: "\FC52";
+}
+.mdi-emoticon-sad-outline::before {
+ content: "\F1F8";
+}
+.mdi-emoticon-tongue::before {
+ content: "\F1F9";
+}
+.mdi-emoticon-tongue-outline::before {
+ content: "\FC53";
+}
+.mdi-emoticon-wink::before {
+ content: "\FC54";
+}
+.mdi-emoticon-wink-outline::before {
+ content: "\FC55";
+}
+.mdi-engine::before {
+ content: "\F1FA";
+}
+.mdi-engine-off::before {
+ content: "\FA45";
+}
+.mdi-engine-off-outline::before {
+ content: "\FA46";
+}
+.mdi-engine-outline::before {
+ content: "\F1FB";
+}
+.mdi-epsilon::before {
+ content: "\F010B";
+}
+.mdi-equal::before {
+ content: "\F1FC";
+}
+.mdi-equal-box::before {
+ content: "\F1FD";
+}
+.mdi-equalizer::before {
+ content: "\FEBF";
+}
+.mdi-equalizer-outline::before {
+ content: "\FEC0";
+}
+.mdi-eraser::before {
+ content: "\F1FE";
+}
+.mdi-eraser-variant::before {
+ content: "\F642";
+}
+.mdi-escalator::before {
+ content: "\F1FF";
+}
+.mdi-escalator-down::before {
+ content: "\F02EB";
+}
+.mdi-escalator-up::before {
+ content: "\F02EA";
+}
+.mdi-eslint::before {
+ content: "\FC56";
+}
+.mdi-et::before {
+ content: "\FAB2";
+}
+.mdi-ethereum::before {
+ content: "\F869";
+}
+.mdi-ethernet::before {
+ content: "\F200";
+}
+.mdi-ethernet-cable::before {
+ content: "\F201";
+}
+.mdi-ethernet-cable-off::before {
+ content: "\F202";
+}
+.mdi-etsy::before {
+ content: "\F203";
+}
+.mdi-ev-station::before {
+ content: "\F5F1";
+}
+.mdi-eventbrite::before {
+ content: "\F7C6";
+}
+.mdi-evernote::before {
+ content: "\F204";
+}
+.mdi-excavator::before {
+ content: "\F0047";
+}
+.mdi-exclamation::before {
+ content: "\F205";
+}
+.mdi-exclamation-thick::before {
+ content: "\F0263";
+}
+.mdi-exit-run::before {
+ content: "\FA47";
+}
+.mdi-exit-to-app::before {
+ content: "\F206";
+}
+.mdi-expand-all::before {
+ content: "\FAB3";
+}
+.mdi-expand-all-outline::before {
+ content: "\FAB4";
+}
+.mdi-expansion-card::before {
+ content: "\F8AD";
+}
+.mdi-expansion-card-variant::before {
+ content: "\FFD2";
+}
+.mdi-exponent::before {
+ content: "\F962";
+}
+.mdi-exponent-box::before {
+ content: "\F963";
+}
+.mdi-export::before {
+ content: "\F207";
+}
+.mdi-export-variant::before {
+ content: "\FB6F";
+}
+.mdi-eye::before {
+ content: "\F208";
+}
+.mdi-eye-check::before {
+ content: "\FCE0";
+}
+.mdi-eye-check-outline::before {
+ content: "\FCE1";
+}
+.mdi-eye-circle::before {
+ content: "\FB70";
+}
+.mdi-eye-circle-outline::before {
+ content: "\FB71";
+}
+.mdi-eye-minus::before {
+ content: "\F0048";
+}
+.mdi-eye-minus-outline::before {
+ content: "\F0049";
+}
+.mdi-eye-off::before {
+ content: "\F209";
+}
+.mdi-eye-off-outline::before {
+ content: "\F6D0";
+}
+.mdi-eye-outline::before {
+ content: "\F6CF";
+}
+.mdi-eye-plus::before {
+ content: "\F86A";
+}
+.mdi-eye-plus-outline::before {
+ content: "\F86B";
+}
+.mdi-eye-settings::before {
+ content: "\F86C";
+}
+.mdi-eye-settings-outline::before {
+ content: "\F86D";
+}
+.mdi-eyedropper::before {
+ content: "\F20A";
+}
+.mdi-eyedropper-variant::before {
+ content: "\F20B";
+}
+.mdi-face::before {
+ content: "\F643";
+}
+.mdi-face-agent::before {
+ content: "\FD4C";
+}
+.mdi-face-outline::before {
+ content: "\FB72";
+}
+.mdi-face-profile::before {
+ content: "\F644";
+}
+.mdi-face-profile-woman::before {
+ content: "\F00A1";
+}
+.mdi-face-recognition::before {
+ content: "\FC57";
+}
+.mdi-face-woman::before {
+ content: "\F00A2";
+}
+.mdi-face-woman-outline::before {
+ content: "\F00A3";
+}
+.mdi-facebook::before {
+ content: "\F20C";
+}
+.mdi-facebook-box::before {
+ content: "\F20D";
+}
+.mdi-facebook-messenger::before {
+ content: "\F20E";
+}
+.mdi-facebook-workplace::before {
+ content: "\FB16";
+}
+.mdi-factory::before {
+ content: "\F20F";
+}
+.mdi-fan::before {
+ content: "\F210";
+}
+.mdi-fan-off::before {
+ content: "\F81C";
+}
+.mdi-fast-forward::before {
+ content: "\F211";
+}
+.mdi-fast-forward-10::before {
+ content: "\FD4D";
+}
+.mdi-fast-forward-30::before {
+ content: "\FCE2";
+}
+.mdi-fast-forward-5::before {
+ content: "\F0223";
+}
+.mdi-fast-forward-outline::before {
+ content: "\F6D1";
+}
+.mdi-fax::before {
+ content: "\F212";
+}
+.mdi-feather::before {
+ content: "\F6D2";
+}
+.mdi-feature-search::before {
+ content: "\FA48";
+}
+.mdi-feature-search-outline::before {
+ content: "\FA49";
+}
+.mdi-fedora::before {
+ content: "\F8DA";
+}
+.mdi-ferris-wheel::before {
+ content: "\FEC1";
+}
+.mdi-ferry::before {
+ content: "\F213";
+}
+.mdi-file::before {
+ content: "\F214";
+}
+.mdi-file-account::before {
+ content: "\F73A";
+}
+.mdi-file-account-outline::before {
+ content: "\F004A";
+}
+.mdi-file-alert::before {
+ content: "\FA4A";
+}
+.mdi-file-alert-outline::before {
+ content: "\FA4B";
+}
+.mdi-file-cabinet::before {
+ content: "\FAB5";
+}
+.mdi-file-cad::before {
+ content: "\FF08";
+}
+.mdi-file-cad-box::before {
+ content: "\FF09";
+}
+.mdi-file-cancel::before {
+ content: "\FDA2";
+}
+.mdi-file-cancel-outline::before {
+ content: "\FDA3";
+}
+.mdi-file-certificate::before {
+ content: "\F01B1";
+}
+.mdi-file-certificate-outline::before {
+ content: "\F01B2";
+}
+.mdi-file-chart::before {
+ content: "\F215";
+}
+.mdi-file-chart-outline::before {
+ content: "\F004B";
+}
+.mdi-file-check::before {
+ content: "\F216";
+}
+.mdi-file-check-outline::before {
+ content: "\FE7B";
+}
+.mdi-file-clock::before {
+ content: "\F030C";
+}
+.mdi-file-clock-outline::before {
+ content: "\F030D";
+}
+.mdi-file-cloud::before {
+ content: "\F217";
+}
+.mdi-file-cloud-outline::before {
+ content: "\F004C";
+}
+.mdi-file-code::before {
+ content: "\F22E";
+}
+.mdi-file-code-outline::before {
+ content: "\F004D";
+}
+.mdi-file-compare::before {
+ content: "\F8A9";
+}
+.mdi-file-delimited::before {
+ content: "\F218";
+}
+.mdi-file-delimited-outline::before {
+ content: "\FEC2";
+}
+.mdi-file-document::before {
+ content: "\F219";
+}
+.mdi-file-document-box::before {
+ content: "\F21A";
+}
+.mdi-file-document-box-check::before {
+ content: "\FEC3";
+}
+.mdi-file-document-box-check-outline::before {
+ content: "\FEC4";
+}
+.mdi-file-document-box-minus::before {
+ content: "\FEC5";
+}
+.mdi-file-document-box-minus-outline::before {
+ content: "\FEC6";
+}
+.mdi-file-document-box-multiple::before {
+ content: "\FAB6";
+}
+.mdi-file-document-box-multiple-outline::before {
+ content: "\FAB7";
+}
+.mdi-file-document-box-outline::before {
+ content: "\F9EC";
+}
+.mdi-file-document-box-plus::before {
+ content: "\FEC7";
+}
+.mdi-file-document-box-plus-outline::before {
+ content: "\FEC8";
+}
+.mdi-file-document-box-remove::before {
+ content: "\FEC9";
+}
+.mdi-file-document-box-remove-outline::before {
+ content: "\FECA";
+}
+.mdi-file-document-box-search::before {
+ content: "\FECB";
+}
+.mdi-file-document-box-search-outline::before {
+ content: "\FECC";
+}
+.mdi-file-document-edit::before {
+ content: "\FDA4";
+}
+.mdi-file-document-edit-outline::before {
+ content: "\FDA5";
+}
+.mdi-file-document-outline::before {
+ content: "\F9ED";
+}
+.mdi-file-download::before {
+ content: "\F964";
+}
+.mdi-file-download-outline::before {
+ content: "\F965";
+}
+.mdi-file-edit::before {
+ content: "\F0212";
+}
+.mdi-file-edit-outline::before {
+ content: "\F0213";
+}
+.mdi-file-excel::before {
+ content: "\F21B";
+}
+.mdi-file-excel-box::before {
+ content: "\F21C";
+}
+.mdi-file-excel-box-outline::before {
+ content: "\F004E";
+}
+.mdi-file-excel-outline::before {
+ content: "\F004F";
+}
+.mdi-file-export::before {
+ content: "\F21D";
+}
+.mdi-file-export-outline::before {
+ content: "\F0050";
+}
+.mdi-file-eye::before {
+ content: "\FDA6";
+}
+.mdi-file-eye-outline::before {
+ content: "\FDA7";
+}
+.mdi-file-find::before {
+ content: "\F21E";
+}
+.mdi-file-find-outline::before {
+ content: "\FB73";
+}
+.mdi-file-hidden::before {
+ content: "\F613";
+}
+.mdi-file-image::before {
+ content: "\F21F";
+}
+.mdi-file-image-outline::before {
+ content: "\FECD";
+}
+.mdi-file-import::before {
+ content: "\F220";
+}
+.mdi-file-import-outline::before {
+ content: "\F0051";
+}
+.mdi-file-key::before {
+ content: "\F01AF";
+}
+.mdi-file-key-outline::before {
+ content: "\F01B0";
+}
+.mdi-file-link::before {
+ content: "\F01A2";
+}
+.mdi-file-link-outline::before {
+ content: "\F01A3";
+}
+.mdi-file-lock::before {
+ content: "\F221";
+}
+.mdi-file-lock-outline::before {
+ content: "\F0052";
+}
+.mdi-file-move::before {
+ content: "\FAB8";
+}
+.mdi-file-move-outline::before {
+ content: "\F0053";
+}
+.mdi-file-multiple::before {
+ content: "\F222";
+}
+.mdi-file-multiple-outline::before {
+ content: "\F0054";
+}
+.mdi-file-music::before {
+ content: "\F223";
+}
+.mdi-file-music-outline::before {
+ content: "\FE7C";
+}
+.mdi-file-outline::before {
+ content: "\F224";
+}
+.mdi-file-pdf::before {
+ content: "\F225";
+}
+.mdi-file-pdf-box::before {
+ content: "\F226";
+}
+.mdi-file-pdf-box-outline::before {
+ content: "\FFD3";
+}
+.mdi-file-pdf-outline::before {
+ content: "\FE7D";
+}
+.mdi-file-percent::before {
+ content: "\F81D";
+}
+.mdi-file-percent-outline::before {
+ content: "\F0055";
+}
+.mdi-file-phone::before {
+ content: "\F01A4";
+}
+.mdi-file-phone-outline::before {
+ content: "\F01A5";
+}
+.mdi-file-plus::before {
+ content: "\F751";
+}
+.mdi-file-plus-outline::before {
+ content: "\FF0A";
+}
+.mdi-file-powerpoint::before {
+ content: "\F227";
+}
+.mdi-file-powerpoint-box::before {
+ content: "\F228";
+}
+.mdi-file-powerpoint-box-outline::before {
+ content: "\F0056";
+}
+.mdi-file-powerpoint-outline::before {
+ content: "\F0057";
+}
+.mdi-file-presentation-box::before {
+ content: "\F229";
+}
+.mdi-file-question::before {
+ content: "\F86E";
+}
+.mdi-file-question-outline::before {
+ content: "\F0058";
+}
+.mdi-file-remove::before {
+ content: "\FB74";
+}
+.mdi-file-remove-outline::before {
+ content: "\F0059";
+}
+.mdi-file-replace::before {
+ content: "\FB17";
+}
+.mdi-file-replace-outline::before {
+ content: "\FB18";
+}
+.mdi-file-restore::before {
+ content: "\F670";
+}
+.mdi-file-restore-outline::before {
+ content: "\F005A";
+}
+.mdi-file-search::before {
+ content: "\FC58";
+}
+.mdi-file-search-outline::before {
+ content: "\FC59";
+}
+.mdi-file-send::before {
+ content: "\F22A";
+}
+.mdi-file-send-outline::before {
+ content: "\F005B";
+}
+.mdi-file-settings::before {
+ content: "\F00A4";
+}
+.mdi-file-settings-outline::before {
+ content: "\F00A5";
+}
+.mdi-file-settings-variant::before {
+ content: "\F00A6";
+}
+.mdi-file-settings-variant-outline::before {
+ content: "\F00A7";
+}
+.mdi-file-star::before {
+ content: "\F005C";
+}
+.mdi-file-star-outline::before {
+ content: "\F005D";
+}
+.mdi-file-swap::before {
+ content: "\FFD4";
+}
+.mdi-file-swap-outline::before {
+ content: "\FFD5";
+}
+.mdi-file-sync::before {
+ content: "\F0241";
+}
+.mdi-file-sync-outline::before {
+ content: "\F0242";
+}
+.mdi-file-table::before {
+ content: "\FC5A";
+}
+.mdi-file-table-box::before {
+ content: "\F010C";
+}
+.mdi-file-table-box-multiple::before {
+ content: "\F010D";
+}
+.mdi-file-table-box-multiple-outline::before {
+ content: "\F010E";
+}
+.mdi-file-table-box-outline::before {
+ content: "\F010F";
+}
+.mdi-file-table-outline::before {
+ content: "\FC5B";
+}
+.mdi-file-tree::before {
+ content: "\F645";
+}
+.mdi-file-undo::before {
+ content: "\F8DB";
+}
+.mdi-file-undo-outline::before {
+ content: "\F005E";
+}
+.mdi-file-upload::before {
+ content: "\FA4C";
+}
+.mdi-file-upload-outline::before {
+ content: "\FA4D";
+}
+.mdi-file-video::before {
+ content: "\F22B";
+}
+.mdi-file-video-outline::before {
+ content: "\FE10";
+}
+.mdi-file-word::before {
+ content: "\F22C";
+}
+.mdi-file-word-box::before {
+ content: "\F22D";
+}
+.mdi-file-word-box-outline::before {
+ content: "\F005F";
+}
+.mdi-file-word-outline::before {
+ content: "\F0060";
+}
+.mdi-film::before {
+ content: "\F22F";
+}
+.mdi-filmstrip::before {
+ content: "\F230";
+}
+.mdi-filmstrip-off::before {
+ content: "\F231";
+}
+.mdi-filter::before {
+ content: "\F232";
+}
+.mdi-filter-menu::before {
+ content: "\F0110";
+}
+.mdi-filter-menu-outline::before {
+ content: "\F0111";
+}
+.mdi-filter-minus::before {
+ content: "\FF0B";
+}
+.mdi-filter-minus-outline::before {
+ content: "\FF0C";
+}
+.mdi-filter-outline::before {
+ content: "\F233";
+}
+.mdi-filter-plus::before {
+ content: "\FF0D";
+}
+.mdi-filter-plus-outline::before {
+ content: "\FF0E";
+}
+.mdi-filter-remove::before {
+ content: "\F234";
+}
+.mdi-filter-remove-outline::before {
+ content: "\F235";
+}
+.mdi-filter-variant::before {
+ content: "\F236";
+}
+.mdi-filter-variant-minus::before {
+ content: "\F013D";
+}
+.mdi-filter-variant-plus::before {
+ content: "\F013E";
+}
+.mdi-filter-variant-remove::before {
+ content: "\F0061";
+}
+.mdi-finance::before {
+ content: "\F81E";
+}
+.mdi-find-replace::before {
+ content: "\F6D3";
+}
+.mdi-fingerprint::before {
+ content: "\F237";
+}
+.mdi-fingerprint-off::before {
+ content: "\FECE";
+}
+.mdi-fire::before {
+ content: "\F238";
+}
+.mdi-fire-extinguisher::before {
+ content: "\FF0F";
+}
+.mdi-fire-hydrant::before {
+ content: "\F0162";
+}
+.mdi-fire-hydrant-alert::before {
+ content: "\F0163";
+}
+.mdi-fire-hydrant-off::before {
+ content: "\F0164";
+}
+.mdi-fire-truck::before {
+ content: "\F8AA";
+}
+.mdi-firebase::before {
+ content: "\F966";
+}
+.mdi-firefox::before {
+ content: "\F239";
+}
+.mdi-fireplace::before {
+ content: "\FE11";
+}
+.mdi-fireplace-off::before {
+ content: "\FE12";
+}
+.mdi-firework::before {
+ content: "\FE13";
+}
+.mdi-fish::before {
+ content: "\F23A";
+}
+.mdi-fishbowl::before {
+ content: "\FF10";
+}
+.mdi-fishbowl-outline::before {
+ content: "\FF11";
+}
+.mdi-fit-to-page::before {
+ content: "\FF12";
+}
+.mdi-fit-to-page-outline::before {
+ content: "\FF13";
+}
+.mdi-flag::before {
+ content: "\F23B";
+}
+.mdi-flag-checkered::before {
+ content: "\F23C";
+}
+.mdi-flag-minus::before {
+ content: "\FB75";
+}
+.mdi-flag-minus-outline::before {
+ content: "\F00DD";
+}
+.mdi-flag-outline::before {
+ content: "\F23D";
+}
+.mdi-flag-plus::before {
+ content: "\FB76";
+}
+.mdi-flag-plus-outline::before {
+ content: "\F00DE";
+}
+.mdi-flag-remove::before {
+ content: "\FB77";
+}
+.mdi-flag-remove-outline::before {
+ content: "\F00DF";
+}
+.mdi-flag-triangle::before {
+ content: "\F23F";
+}
+.mdi-flag-variant::before {
+ content: "\F240";
+}
+.mdi-flag-variant-outline::before {
+ content: "\F23E";
+}
+.mdi-flare::before {
+ content: "\FD4E";
+}
+.mdi-flash::before {
+ content: "\F241";
+}
+.mdi-flash-alert::before {
+ content: "\FF14";
+}
+.mdi-flash-alert-outline::before {
+ content: "\FF15";
+}
+.mdi-flash-auto::before {
+ content: "\F242";
+}
+.mdi-flash-circle::before {
+ content: "\F81F";
+}
+.mdi-flash-off::before {
+ content: "\F243";
+}
+.mdi-flash-outline::before {
+ content: "\F6D4";
+}
+.mdi-flash-red-eye::before {
+ content: "\F67A";
+}
+.mdi-flashlight::before {
+ content: "\F244";
+}
+.mdi-flashlight-off::before {
+ content: "\F245";
+}
+.mdi-flask::before {
+ content: "\F093";
+}
+.mdi-flask-empty::before {
+ content: "\F094";
+}
+.mdi-flask-empty-minus::before {
+ content: "\F0265";
+}
+.mdi-flask-empty-minus-outline::before {
+ content: "\F0266";
+}
+.mdi-flask-empty-outline::before {
+ content: "\F095";
+}
+.mdi-flask-empty-plus::before {
+ content: "\F0267";
+}
+.mdi-flask-empty-plus-outline::before {
+ content: "\F0268";
+}
+.mdi-flask-empty-remove::before {
+ content: "\F0269";
+}
+.mdi-flask-empty-remove-outline::before {
+ content: "\F026A";
+}
+.mdi-flask-minus::before {
+ content: "\F026B";
+}
+.mdi-flask-minus-outline::before {
+ content: "\F026C";
+}
+.mdi-flask-outline::before {
+ content: "\F096";
+}
+.mdi-flask-plus::before {
+ content: "\F026D";
+}
+.mdi-flask-plus-outline::before {
+ content: "\F026E";
+}
+.mdi-flask-remove::before {
+ content: "\F026F";
+}
+.mdi-flask-remove-outline::before {
+ content: "\F0270";
+}
+.mdi-flask-round-bottom::before {
+ content: "\F0276";
+}
+.mdi-flask-round-bottom-empty::before {
+ content: "\F0277";
+}
+.mdi-flask-round-bottom-empty-outline::before {
+ content: "\F0278";
+}
+.mdi-flask-round-bottom-outline::before {
+ content: "\F0279";
+}
+.mdi-flattr::before {
+ content: "\F246";
+}
+.mdi-fleur-de-lis::before {
+ content: "\F032E";
+}
+.mdi-flickr::before {
+ content: "\FCE3";
+}
+.mdi-flip-horizontal::before {
+ content: "\F0112";
+}
+.mdi-flip-to-back::before {
+ content: "\F247";
+}
+.mdi-flip-to-front::before {
+ content: "\F248";
+}
+.mdi-flip-vertical::before {
+ content: "\F0113";
+}
+.mdi-floor-lamp::before {
+ content: "\F8DC";
+}
+.mdi-floor-lamp-dual::before {
+ content: "\F0062";
+}
+.mdi-floor-lamp-variant::before {
+ content: "\F0063";
+}
+.mdi-floor-plan::before {
+ content: "\F820";
+}
+.mdi-floppy::before {
+ content: "\F249";
+}
+.mdi-floppy-variant::before {
+ content: "\F9EE";
+}
+.mdi-flower::before {
+ content: "\F24A";
+}
+.mdi-flower-outline::before {
+ content: "\F9EF";
+}
+.mdi-flower-poppy::before {
+ content: "\FCE4";
+}
+.mdi-flower-tulip::before {
+ content: "\F9F0";
+}
+.mdi-flower-tulip-outline::before {
+ content: "\F9F1";
+}
+.mdi-focus-auto::before {
+ content: "\FF6B";
+}
+.mdi-focus-field::before {
+ content: "\FF6C";
+}
+.mdi-focus-field-horizontal::before {
+ content: "\FF6D";
+}
+.mdi-focus-field-vertical::before {
+ content: "\FF6E";
+}
+.mdi-folder::before {
+ content: "\F24B";
+}
+.mdi-folder-account::before {
+ content: "\F24C";
+}
+.mdi-folder-account-outline::before {
+ content: "\FB78";
+}
+.mdi-folder-alert::before {
+ content: "\FDA8";
+}
+.mdi-folder-alert-outline::before {
+ content: "\FDA9";
+}
+.mdi-folder-clock::before {
+ content: "\FAB9";
+}
+.mdi-folder-clock-outline::before {
+ content: "\FABA";
+}
+.mdi-folder-download::before {
+ content: "\F24D";
+}
+.mdi-folder-download-outline::before {
+ content: "\F0114";
+}
+.mdi-folder-edit::before {
+ content: "\F8DD";
+}
+.mdi-folder-edit-outline::before {
+ content: "\FDAA";
+}
+.mdi-folder-google-drive::before {
+ content: "\F24E";
+}
+.mdi-folder-heart::before {
+ content: "\F0115";
+}
+.mdi-folder-heart-outline::before {
+ content: "\F0116";
+}
+.mdi-folder-home::before {
+ content: "\F00E0";
+}
+.mdi-folder-home-outline::before {
+ content: "\F00E1";
+}
+.mdi-folder-image::before {
+ content: "\F24F";
+}
+.mdi-folder-information::before {
+ content: "\F00E2";
+}
+.mdi-folder-information-outline::before {
+ content: "\F00E3";
+}
+.mdi-folder-key::before {
+ content: "\F8AB";
+}
+.mdi-folder-key-network::before {
+ content: "\F8AC";
+}
+.mdi-folder-key-network-outline::before {
+ content: "\FC5C";
+}
+.mdi-folder-key-outline::before {
+ content: "\F0117";
+}
+.mdi-folder-lock::before {
+ content: "\F250";
+}
+.mdi-folder-lock-open::before {
+ content: "\F251";
+}
+.mdi-folder-marker::before {
+ content: "\F0298";
+}
+.mdi-folder-marker-outline::before {
+ content: "\F0299";
+}
+.mdi-folder-move::before {
+ content: "\F252";
+}
+.mdi-folder-move-outline::before {
+ content: "\F0271";
+}
+.mdi-folder-multiple::before {
+ content: "\F253";
+}
+.mdi-folder-multiple-image::before {
+ content: "\F254";
+}
+.mdi-folder-multiple-outline::before {
+ content: "\F255";
+}
+.mdi-folder-music::before {
+ content: "\F0384";
+}
+.mdi-folder-music-outline::before {
+ content: "\F0385";
+}
+.mdi-folder-network::before {
+ content: "\F86F";
+}
+.mdi-folder-network-outline::before {
+ content: "\FC5D";
+}
+.mdi-folder-open::before {
+ content: "\F76F";
+}
+.mdi-folder-open-outline::before {
+ content: "\FDAB";
+}
+.mdi-folder-outline::before {
+ content: "\F256";
+}
+.mdi-folder-plus::before {
+ content: "\F257";
+}
+.mdi-folder-plus-outline::before {
+ content: "\FB79";
+}
+.mdi-folder-pound::before {
+ content: "\FCE5";
+}
+.mdi-folder-pound-outline::before {
+ content: "\FCE6";
+}
+.mdi-folder-remove::before {
+ content: "\F258";
+}
+.mdi-folder-remove-outline::before {
+ content: "\FB7A";
+}
+.mdi-folder-search::before {
+ content: "\F967";
+}
+.mdi-folder-search-outline::before {
+ content: "\F968";
+}
+.mdi-folder-settings::before {
+ content: "\F00A8";
+}
+.mdi-folder-settings-outline::before {
+ content: "\F00A9";
+}
+.mdi-folder-settings-variant::before {
+ content: "\F00AA";
+}
+.mdi-folder-settings-variant-outline::before {
+ content: "\F00AB";
+}
+.mdi-folder-star::before {
+ content: "\F69C";
+}
+.mdi-folder-star-outline::before {
+ content: "\FB7B";
+}
+.mdi-folder-swap::before {
+ content: "\FFD6";
+}
+.mdi-folder-swap-outline::before {
+ content: "\FFD7";
+}
+.mdi-folder-sync::before {
+ content: "\FCE7";
+}
+.mdi-folder-sync-outline::before {
+ content: "\FCE8";
+}
+.mdi-folder-table::before {
+ content: "\F030E";
+}
+.mdi-folder-table-outline::before {
+ content: "\F030F";
+}
+.mdi-folder-text::before {
+ content: "\FC5E";
+}
+.mdi-folder-text-outline::before {
+ content: "\FC5F";
+}
+.mdi-folder-upload::before {
+ content: "\F259";
+}
+.mdi-folder-upload-outline::before {
+ content: "\F0118";
+}
+.mdi-folder-zip::before {
+ content: "\F6EA";
+}
+.mdi-folder-zip-outline::before {
+ content: "\F7B8";
+}
+.mdi-font-awesome::before {
+ content: "\F03A";
+}
+.mdi-food::before {
+ content: "\F25A";
+}
+.mdi-food-apple::before {
+ content: "\F25B";
+}
+.mdi-food-apple-outline::before {
+ content: "\FC60";
+}
+.mdi-food-croissant::before {
+ content: "\F7C7";
+}
+.mdi-food-fork-drink::before {
+ content: "\F5F2";
+}
+.mdi-food-off::before {
+ content: "\F5F3";
+}
+.mdi-food-variant::before {
+ content: "\F25C";
+}
+.mdi-foot-print::before {
+ content: "\FF6F";
+}
+.mdi-football::before {
+ content: "\F25D";
+}
+.mdi-football-australian::before {
+ content: "\F25E";
+}
+.mdi-football-helmet::before {
+ content: "\F25F";
+}
+.mdi-forklift::before {
+ content: "\F7C8";
+}
+.mdi-format-align-bottom::before {
+ content: "\F752";
+}
+.mdi-format-align-center::before {
+ content: "\F260";
+}
+.mdi-format-align-justify::before {
+ content: "\F261";
+}
+.mdi-format-align-left::before {
+ content: "\F262";
+}
+.mdi-format-align-middle::before {
+ content: "\F753";
+}
+.mdi-format-align-right::before {
+ content: "\F263";
+}
+.mdi-format-align-top::before {
+ content: "\F754";
+}
+.mdi-format-annotation-minus::before {
+ content: "\FABB";
+}
+.mdi-format-annotation-plus::before {
+ content: "\F646";
+}
+.mdi-format-bold::before {
+ content: "\F264";
+}
+.mdi-format-clear::before {
+ content: "\F265";
+}
+.mdi-format-color-fill::before {
+ content: "\F266";
+}
+.mdi-format-color-highlight::before {
+ content: "\FE14";
+}
+.mdi-format-color-marker-cancel::before {
+ content: "\F033E";
+}
+.mdi-format-color-text::before {
+ content: "\F69D";
+}
+.mdi-format-columns::before {
+ content: "\F8DE";
+}
+.mdi-format-float-center::before {
+ content: "\F267";
+}
+.mdi-format-float-left::before {
+ content: "\F268";
+}
+.mdi-format-float-none::before {
+ content: "\F269";
+}
+.mdi-format-float-right::before {
+ content: "\F26A";
+}
+.mdi-format-font::before {
+ content: "\F6D5";
+}
+.mdi-format-font-size-decrease::before {
+ content: "\F9F2";
+}
+.mdi-format-font-size-increase::before {
+ content: "\F9F3";
+}
+.mdi-format-header-1::before {
+ content: "\F26B";
+}
+.mdi-format-header-2::before {
+ content: "\F26C";
+}
+.mdi-format-header-3::before {
+ content: "\F26D";
+}
+.mdi-format-header-4::before {
+ content: "\F26E";
+}
+.mdi-format-header-5::before {
+ content: "\F26F";
+}
+.mdi-format-header-6::before {
+ content: "\F270";
+}
+.mdi-format-header-decrease::before {
+ content: "\F271";
+}
+.mdi-format-header-equal::before {
+ content: "\F272";
+}
+.mdi-format-header-increase::before {
+ content: "\F273";
+}
+.mdi-format-header-pound::before {
+ content: "\F274";
+}
+.mdi-format-horizontal-align-center::before {
+ content: "\F61E";
+}
+.mdi-format-horizontal-align-left::before {
+ content: "\F61F";
+}
+.mdi-format-horizontal-align-right::before {
+ content: "\F620";
+}
+.mdi-format-indent-decrease::before {
+ content: "\F275";
+}
+.mdi-format-indent-increase::before {
+ content: "\F276";
+}
+.mdi-format-italic::before {
+ content: "\F277";
+}
+.mdi-format-letter-case::before {
+ content: "\FB19";
+}
+.mdi-format-letter-case-lower::before {
+ content: "\FB1A";
+}
+.mdi-format-letter-case-upper::before {
+ content: "\FB1B";
+}
+.mdi-format-letter-ends-with::before {
+ content: "\FFD8";
+}
+.mdi-format-letter-matches::before {
+ content: "\FFD9";
+}
+.mdi-format-letter-starts-with::before {
+ content: "\FFDA";
+}
+.mdi-format-line-spacing::before {
+ content: "\F278";
+}
+.mdi-format-line-style::before {
+ content: "\F5C8";
+}
+.mdi-format-line-weight::before {
+ content: "\F5C9";
+}
+.mdi-format-list-bulleted::before {
+ content: "\F279";
+}
+.mdi-format-list-bulleted-square::before {
+ content: "\FDAC";
+}
+.mdi-format-list-bulleted-triangle::before {
+ content: "\FECF";
+}
+.mdi-format-list-bulleted-type::before {
+ content: "\F27A";
+}
+.mdi-format-list-checkbox::before {
+ content: "\F969";
+}
+.mdi-format-list-checks::before {
+ content: "\F755";
+}
+.mdi-format-list-numbered::before {
+ content: "\F27B";
+}
+.mdi-format-list-numbered-rtl::before {
+ content: "\FCE9";
+}
+.mdi-format-list-text::before {
+ content: "\F029A";
+}
+.mdi-format-overline::before {
+ content: "\FED0";
+}
+.mdi-format-page-break::before {
+ content: "\F6D6";
+}
+.mdi-format-paint::before {
+ content: "\F27C";
+}
+.mdi-format-paragraph::before {
+ content: "\F27D";
+}
+.mdi-format-pilcrow::before {
+ content: "\F6D7";
+}
+.mdi-format-quote-close::before {
+ content: "\F27E";
+}
+.mdi-format-quote-close-outline::before {
+ content: "\F01D3";
+}
+.mdi-format-quote-open::before {
+ content: "\F756";
+}
+.mdi-format-quote-open-outline::before {
+ content: "\F01D2";
+}
+.mdi-format-rotate-90::before {
+ content: "\F6A9";
+}
+.mdi-format-section::before {
+ content: "\F69E";
+}
+.mdi-format-size::before {
+ content: "\F27F";
+}
+.mdi-format-strikethrough::before {
+ content: "\F280";
+}
+.mdi-format-strikethrough-variant::before {
+ content: "\F281";
+}
+.mdi-format-subscript::before {
+ content: "\F282";
+}
+.mdi-format-superscript::before {
+ content: "\F283";
+}
+.mdi-format-text::before {
+ content: "\F284";
+}
+.mdi-format-text-rotation-angle-down::before {
+ content: "\FFDB";
+}
+.mdi-format-text-rotation-angle-up::before {
+ content: "\FFDC";
+}
+.mdi-format-text-rotation-down::before {
+ content: "\FD4F";
+}
+.mdi-format-text-rotation-down-vertical::before {
+ content: "\FFDD";
+}
+.mdi-format-text-rotation-none::before {
+ content: "\FD50";
+}
+.mdi-format-text-rotation-up::before {
+ content: "\FFDE";
+}
+.mdi-format-text-rotation-vertical::before {
+ content: "\FFDF";
+}
+.mdi-format-text-variant::before {
+ content: "\FE15";
+}
+.mdi-format-text-wrapping-clip::before {
+ content: "\FCEA";
+}
+.mdi-format-text-wrapping-overflow::before {
+ content: "\FCEB";
+}
+.mdi-format-text-wrapping-wrap::before {
+ content: "\FCEC";
+}
+.mdi-format-textbox::before {
+ content: "\FCED";
+}
+.mdi-format-textdirection-l-to-r::before {
+ content: "\F285";
+}
+.mdi-format-textdirection-r-to-l::before {
+ content: "\F286";
+}
+.mdi-format-title::before {
+ content: "\F5F4";
+}
+.mdi-format-underline::before {
+ content: "\F287";
+}
+.mdi-format-vertical-align-bottom::before {
+ content: "\F621";
+}
+.mdi-format-vertical-align-center::before {
+ content: "\F622";
+}
+.mdi-format-vertical-align-top::before {
+ content: "\F623";
+}
+.mdi-format-wrap-inline::before {
+ content: "\F288";
+}
+.mdi-format-wrap-square::before {
+ content: "\F289";
+}
+.mdi-format-wrap-tight::before {
+ content: "\F28A";
+}
+.mdi-format-wrap-top-bottom::before {
+ content: "\F28B";
+}
+.mdi-forum::before {
+ content: "\F28C";
+}
+.mdi-forum-outline::before {
+ content: "\F821";
+}
+.mdi-forward::before {
+ content: "\F28D";
+}
+.mdi-forwardburger::before {
+ content: "\FD51";
+}
+.mdi-fountain::before {
+ content: "\F96A";
+}
+.mdi-fountain-pen::before {
+ content: "\FCEE";
+}
+.mdi-fountain-pen-tip::before {
+ content: "\FCEF";
+}
+.mdi-foursquare::before {
+ content: "\F28E";
+}
+.mdi-freebsd::before {
+ content: "\F8DF";
+}
+.mdi-frequently-asked-questions::before {
+ content: "\FED1";
+}
+.mdi-fridge::before {
+ content: "\F290";
+}
+.mdi-fridge-alert::before {
+ content: "\F01DC";
+}
+.mdi-fridge-alert-outline::before {
+ content: "\F01DD";
+}
+.mdi-fridge-bottom::before {
+ content: "\F292";
+}
+.mdi-fridge-off::before {
+ content: "\F01DA";
+}
+.mdi-fridge-off-outline::before {
+ content: "\F01DB";
+}
+.mdi-fridge-outline::before {
+ content: "\F28F";
+}
+.mdi-fridge-top::before {
+ content: "\F291";
+}
+.mdi-fruit-cherries::before {
+ content: "\F0064";
+}
+.mdi-fruit-citrus::before {
+ content: "\F0065";
+}
+.mdi-fruit-grapes::before {
+ content: "\F0066";
+}
+.mdi-fruit-grapes-outline::before {
+ content: "\F0067";
+}
+.mdi-fruit-pineapple::before {
+ content: "\F0068";
+}
+.mdi-fruit-watermelon::before {
+ content: "\F0069";
+}
+.mdi-fuel::before {
+ content: "\F7C9";
+}
+.mdi-fullscreen::before {
+ content: "\F293";
+}
+.mdi-fullscreen-exit::before {
+ content: "\F294";
+}
+.mdi-function::before {
+ content: "\F295";
+}
+.mdi-function-variant::before {
+ content: "\F870";
+}
+.mdi-furigana-horizontal::before {
+ content: "\F00AC";
+}
+.mdi-furigana-vertical::before {
+ content: "\F00AD";
+}
+.mdi-fuse::before {
+ content: "\FC61";
+}
+.mdi-fuse-blade::before {
+ content: "\FC62";
+}
+.mdi-gamepad::before {
+ content: "\F296";
+}
+.mdi-gamepad-circle::before {
+ content: "\FE16";
+}
+.mdi-gamepad-circle-down::before {
+ content: "\FE17";
+}
+.mdi-gamepad-circle-left::before {
+ content: "\FE18";
+}
+.mdi-gamepad-circle-outline::before {
+ content: "\FE19";
+}
+.mdi-gamepad-circle-right::before {
+ content: "\FE1A";
+}
+.mdi-gamepad-circle-up::before {
+ content: "\FE1B";
+}
+.mdi-gamepad-down::before {
+ content: "\FE1C";
+}
+.mdi-gamepad-left::before {
+ content: "\FE1D";
+}
+.mdi-gamepad-right::before {
+ content: "\FE1E";
+}
+.mdi-gamepad-round::before {
+ content: "\FE1F";
+}
+.mdi-gamepad-round-down::before {
+ content: "\FE7E";
+}
+.mdi-gamepad-round-left::before {
+ content: "\FE7F";
+}
+.mdi-gamepad-round-outline::before {
+ content: "\FE80";
+}
+.mdi-gamepad-round-right::before {
+ content: "\FE81";
+}
+.mdi-gamepad-round-up::before {
+ content: "\FE82";
+}
+.mdi-gamepad-square::before {
+ content: "\FED2";
+}
+.mdi-gamepad-square-outline::before {
+ content: "\FED3";
+}
+.mdi-gamepad-up::before {
+ content: "\FE83";
+}
+.mdi-gamepad-variant::before {
+ content: "\F297";
+}
+.mdi-gamepad-variant-outline::before {
+ content: "\FED4";
+}
+.mdi-gamma::before {
+ content: "\F0119";
+}
+.mdi-gantry-crane::before {
+ content: "\FDAD";
+}
+.mdi-garage::before {
+ content: "\F6D8";
+}
+.mdi-garage-alert::before {
+ content: "\F871";
+}
+.mdi-garage-alert-variant::before {
+ content: "\F0300";
+}
+.mdi-garage-open::before {
+ content: "\F6D9";
+}
+.mdi-garage-open-variant::before {
+ content: "\F02FF";
+}
+.mdi-garage-variant::before {
+ content: "\F02FE";
+}
+.mdi-gas-cylinder::before {
+ content: "\F647";
+}
+.mdi-gas-station::before {
+ content: "\F298";
+}
+.mdi-gas-station-outline::before {
+ content: "\FED5";
+}
+.mdi-gate::before {
+ content: "\F299";
+}
+.mdi-gate-and::before {
+ content: "\F8E0";
+}
+.mdi-gate-arrow-right::before {
+ content: "\F0194";
+}
+.mdi-gate-nand::before {
+ content: "\F8E1";
+}
+.mdi-gate-nor::before {
+ content: "\F8E2";
+}
+.mdi-gate-not::before {
+ content: "\F8E3";
+}
+.mdi-gate-open::before {
+ content: "\F0195";
+}
+.mdi-gate-or::before {
+ content: "\F8E4";
+}
+.mdi-gate-xnor::before {
+ content: "\F8E5";
+}
+.mdi-gate-xor::before {
+ content: "\F8E6";
+}
+.mdi-gatsby::before {
+ content: "\FE84";
+}
+.mdi-gauge::before {
+ content: "\F29A";
+}
+.mdi-gauge-empty::before {
+ content: "\F872";
+}
+.mdi-gauge-full::before {
+ content: "\F873";
+}
+.mdi-gauge-low::before {
+ content: "\F874";
+}
+.mdi-gavel::before {
+ content: "\F29B";
+}
+.mdi-gender-female::before {
+ content: "\F29C";
+}
+.mdi-gender-male::before {
+ content: "\F29D";
+}
+.mdi-gender-male-female::before {
+ content: "\F29E";
+}
+.mdi-gender-male-female-variant::before {
+ content: "\F016A";
+}
+.mdi-gender-non-binary::before {
+ content: "\F016B";
+}
+.mdi-gender-transgender::before {
+ content: "\F29F";
+}
+.mdi-gentoo::before {
+ content: "\F8E7";
+}
+.mdi-gesture::before {
+ content: "\F7CA";
+}
+.mdi-gesture-double-tap::before {
+ content: "\F73B";
+}
+.mdi-gesture-pinch::before {
+ content: "\FABC";
+}
+.mdi-gesture-spread::before {
+ content: "\FABD";
+}
+.mdi-gesture-swipe::before {
+ content: "\FD52";
+}
+.mdi-gesture-swipe-down::before {
+ content: "\F73C";
+}
+.mdi-gesture-swipe-horizontal::before {
+ content: "\FABE";
+}
+.mdi-gesture-swipe-left::before {
+ content: "\F73D";
+}
+.mdi-gesture-swipe-right::before {
+ content: "\F73E";
+}
+.mdi-gesture-swipe-up::before {
+ content: "\F73F";
+}
+.mdi-gesture-swipe-vertical::before {
+ content: "\FABF";
+}
+.mdi-gesture-tap::before {
+ content: "\F740";
+}
+.mdi-gesture-tap-box::before {
+ content: "\F02D4";
+}
+.mdi-gesture-tap-button::before {
+ content: "\F02D3";
+}
+.mdi-gesture-tap-hold::before {
+ content: "\FD53";
+}
+.mdi-gesture-two-double-tap::before {
+ content: "\F741";
+}
+.mdi-gesture-two-tap::before {
+ content: "\F742";
+}
+.mdi-ghost::before {
+ content: "\F2A0";
+}
+.mdi-ghost-off::before {
+ content: "\F9F4";
+}
+.mdi-gif::before {
+ content: "\FD54";
+}
+.mdi-gift::before {
+ content: "\FE85";
+}
+.mdi-gift-outline::before {
+ content: "\F2A1";
+}
+.mdi-git::before {
+ content: "\F2A2";
+}
+.mdi-github-box::before {
+ content: "\F2A3";
+}
+.mdi-github-circle::before {
+ content: "\F2A4";
+}
+.mdi-github-face::before {
+ content: "\F6DA";
+}
+.mdi-gitlab::before {
+ content: "\FB7C";
+}
+.mdi-glass-cocktail::before {
+ content: "\F356";
+}
+.mdi-glass-flute::before {
+ content: "\F2A5";
+}
+.mdi-glass-mug::before {
+ content: "\F2A6";
+}
+.mdi-glass-mug-variant::before {
+ content: "\F0141";
+}
+.mdi-glass-pint-outline::before {
+ content: "\F0338";
+}
+.mdi-glass-stange::before {
+ content: "\F2A7";
+}
+.mdi-glass-tulip::before {
+ content: "\F2A8";
+}
+.mdi-glass-wine::before {
+ content: "\F875";
+}
+.mdi-glassdoor::before {
+ content: "\F2A9";
+}
+.mdi-glasses::before {
+ content: "\F2AA";
+}
+.mdi-globe-light::before {
+ content: "\F0302";
+}
+.mdi-globe-model::before {
+ content: "\F8E8";
+}
+.mdi-gmail::before {
+ content: "\F2AB";
+}
+.mdi-gnome::before {
+ content: "\F2AC";
+}
+.mdi-go-kart::before {
+ content: "\FD55";
+}
+.mdi-go-kart-track::before {
+ content: "\FD56";
+}
+.mdi-gog::before {
+ content: "\FB7D";
+}
+.mdi-gold::before {
+ content: "\F027A";
+}
+.mdi-golf::before {
+ content: "\F822";
+}
+.mdi-golf-cart::before {
+ content: "\F01CF";
+}
+.mdi-golf-tee::before {
+ content: "\F00AE";
+}
+.mdi-gondola::before {
+ content: "\F685";
+}
+.mdi-goodreads::before {
+ content: "\FD57";
+}
+.mdi-google::before {
+ content: "\F2AD";
+}
+.mdi-google-adwords::before {
+ content: "\FC63";
+}
+.mdi-google-analytics::before {
+ content: "\F7CB";
+}
+.mdi-google-assistant::before {
+ content: "\F7CC";
+}
+.mdi-google-cardboard::before {
+ content: "\F2AE";
+}
+.mdi-google-chrome::before {
+ content: "\F2AF";
+}
+.mdi-google-circles::before {
+ content: "\F2B0";
+}
+.mdi-google-circles-communities::before {
+ content: "\F2B1";
+}
+.mdi-google-circles-extended::before {
+ content: "\F2B2";
+}
+.mdi-google-circles-group::before {
+ content: "\F2B3";
+}
+.mdi-google-classroom::before {
+ content: "\F2C0";
+}
+.mdi-google-cloud::before {
+ content: "\F0221";
+}
+.mdi-google-controller::before {
+ content: "\F2B4";
+}
+.mdi-google-controller-off::before {
+ content: "\F2B5";
+}
+.mdi-google-downasaur::before {
+ content: "\F038D";
+}
+.mdi-google-drive::before {
+ content: "\F2B6";
+}
+.mdi-google-earth::before {
+ content: "\F2B7";
+}
+.mdi-google-fit::before {
+ content: "\F96B";
+}
+.mdi-google-glass::before {
+ content: "\F2B8";
+}
+.mdi-google-hangouts::before {
+ content: "\F2C9";
+}
+.mdi-google-home::before {
+ content: "\F823";
+}
+.mdi-google-keep::before {
+ content: "\F6DB";
+}
+.mdi-google-lens::before {
+ content: "\F9F5";
+}
+.mdi-google-maps::before {
+ content: "\F5F5";
+}
+.mdi-google-my-business::before {
+ content: "\F006A";
+}
+.mdi-google-nearby::before {
+ content: "\F2B9";
+}
+.mdi-google-pages::before {
+ content: "\F2BA";
+}
+.mdi-google-photos::before {
+ content: "\F6DC";
+}
+.mdi-google-physical-web::before {
+ content: "\F2BB";
+}
+.mdi-google-play::before {
+ content: "\F2BC";
+}
+.mdi-google-plus::before {
+ content: "\F2BD";
+}
+.mdi-google-plus-box::before {
+ content: "\F2BE";
+}
+.mdi-google-podcast::before {
+ content: "\FED6";
+}
+.mdi-google-spreadsheet::before {
+ content: "\F9F6";
+}
+.mdi-google-street-view::before {
+ content: "\FC64";
+}
+.mdi-google-translate::before {
+ content: "\F2BF";
+}
+.mdi-gradient::before {
+ content: "\F69F";
+}
+.mdi-grain::before {
+ content: "\FD58";
+}
+.mdi-graph::before {
+ content: "\F006B";
+}
+.mdi-graph-outline::before {
+ content: "\F006C";
+}
+.mdi-graphql::before {
+ content: "\F876";
+}
+.mdi-grave-stone::before {
+ content: "\FB7E";
+}
+.mdi-grease-pencil::before {
+ content: "\F648";
+}
+.mdi-greater-than::before {
+ content: "\F96C";
+}
+.mdi-greater-than-or-equal::before {
+ content: "\F96D";
+}
+.mdi-grid::before {
+ content: "\F2C1";
+}
+.mdi-grid-large::before {
+ content: "\F757";
+}
+.mdi-grid-off::before {
+ content: "\F2C2";
+}
+.mdi-grill::before {
+ content: "\FE86";
+}
+.mdi-grill-outline::before {
+ content: "\F01B5";
+}
+.mdi-group::before {
+ content: "\F2C3";
+}
+.mdi-guitar-acoustic::before {
+ content: "\F770";
+}
+.mdi-guitar-electric::before {
+ content: "\F2C4";
+}
+.mdi-guitar-pick::before {
+ content: "\F2C5";
+}
+.mdi-guitar-pick-outline::before {
+ content: "\F2C6";
+}
+.mdi-guy-fawkes-mask::before {
+ content: "\F824";
+}
+.mdi-hackernews::before {
+ content: "\F624";
+}
+.mdi-hail::before {
+ content: "\FAC0";
+}
+.mdi-hair-dryer::before {
+ content: "\F011A";
+}
+.mdi-hair-dryer-outline::before {
+ content: "\F011B";
+}
+.mdi-halloween::before {
+ content: "\FB7F";
+}
+.mdi-hamburger::before {
+ content: "\F684";
+}
+.mdi-hammer::before {
+ content: "\F8E9";
+}
+.mdi-hammer-screwdriver::before {
+ content: "\F034D";
+}
+.mdi-hammer-wrench::before {
+ content: "\F034E";
+}
+.mdi-hand::before {
+ content: "\FA4E";
+}
+.mdi-hand-heart::before {
+ content: "\F011C";
+}
+.mdi-hand-left::before {
+ content: "\FE87";
+}
+.mdi-hand-okay::before {
+ content: "\FA4F";
+}
+.mdi-hand-peace::before {
+ content: "\FA50";
+}
+.mdi-hand-peace-variant::before {
+ content: "\FA51";
+}
+.mdi-hand-pointing-down::before {
+ content: "\FA52";
+}
+.mdi-hand-pointing-left::before {
+ content: "\FA53";
+}
+.mdi-hand-pointing-right::before {
+ content: "\F2C7";
+}
+.mdi-hand-pointing-up::before {
+ content: "\FA54";
+}
+.mdi-hand-right::before {
+ content: "\FE88";
+}
+.mdi-hand-saw::before {
+ content: "\FE89";
+}
+.mdi-handball::before {
+ content: "\FF70";
+}
+.mdi-handcuffs::before {
+ content: "\F0169";
+}
+.mdi-handshake::before {
+ content: "\F0243";
+}
+.mdi-hanger::before {
+ content: "\F2C8";
+}
+.mdi-hard-hat::before {
+ content: "\F96E";
+}
+.mdi-harddisk::before {
+ content: "\F2CA";
+}
+.mdi-harddisk-plus::before {
+ content: "\F006D";
+}
+.mdi-harddisk-remove::before {
+ content: "\F006E";
+}
+.mdi-hat-fedora::before {
+ content: "\FB80";
+}
+.mdi-hazard-lights::before {
+ content: "\FC65";
+}
+.mdi-hdr::before {
+ content: "\FD59";
+}
+.mdi-hdr-off::before {
+ content: "\FD5A";
+}
+.mdi-head::before {
+ content: "\F0389";
+}
+.mdi-head-alert::before {
+ content: "\F0363";
+}
+.mdi-head-alert-outline::before {
+ content: "\F0364";
+}
+.mdi-head-check::before {
+ content: "\F0365";
+}
+.mdi-head-check-outline::before {
+ content: "\F0366";
+}
+.mdi-head-cog::before {
+ content: "\F0367";
+}
+.mdi-head-cog-outline::before {
+ content: "\F0368";
+}
+.mdi-head-dots-horizontal::before {
+ content: "\F0369";
+}
+.mdi-head-dots-horizontal-outline::before {
+ content: "\F036A";
+}
+.mdi-head-flash::before {
+ content: "\F036B";
+}
+.mdi-head-flash-outline::before {
+ content: "\F036C";
+}
+.mdi-head-heart::before {
+ content: "\F036D";
+}
+.mdi-head-heart-outline::before {
+ content: "\F036E";
+}
+.mdi-head-lightbulb::before {
+ content: "\F036F";
+}
+.mdi-head-lightbulb-outline::before {
+ content: "\F0370";
+}
+.mdi-head-minus::before {
+ content: "\F0371";
+}
+.mdi-head-minus-outline::before {
+ content: "\F0372";
+}
+.mdi-head-outline::before {
+ content: "\F038A";
+}
+.mdi-head-plus::before {
+ content: "\F0373";
+}
+.mdi-head-plus-outline::before {
+ content: "\F0374";
+}
+.mdi-head-question::before {
+ content: "\F0375";
+}
+.mdi-head-question-outline::before {
+ content: "\F0376";
+}
+.mdi-head-remove::before {
+ content: "\F0377";
+}
+.mdi-head-remove-outline::before {
+ content: "\F0378";
+}
+.mdi-head-snowflake::before {
+ content: "\F0379";
+}
+.mdi-head-snowflake-outline::before {
+ content: "\F037A";
+}
+.mdi-head-sync::before {
+ content: "\F037B";
+}
+.mdi-head-sync-outline::before {
+ content: "\F037C";
+}
+.mdi-headphones::before {
+ content: "\F2CB";
+}
+.mdi-headphones-bluetooth::before {
+ content: "\F96F";
+}
+.mdi-headphones-box::before {
+ content: "\F2CC";
+}
+.mdi-headphones-off::before {
+ content: "\F7CD";
+}
+.mdi-headphones-settings::before {
+ content: "\F2CD";
+}
+.mdi-headset::before {
+ content: "\F2CE";
+}
+.mdi-headset-dock::before {
+ content: "\F2CF";
+}
+.mdi-headset-off::before {
+ content: "\F2D0";
+}
+.mdi-heart::before {
+ content: "\F2D1";
+}
+.mdi-heart-box::before {
+ content: "\F2D2";
+}
+.mdi-heart-box-outline::before {
+ content: "\F2D3";
+}
+.mdi-heart-broken::before {
+ content: "\F2D4";
+}
+.mdi-heart-broken-outline::before {
+ content: "\FCF0";
+}
+.mdi-heart-circle::before {
+ content: "\F970";
+}
+.mdi-heart-circle-outline::before {
+ content: "\F971";
+}
+.mdi-heart-flash::before {
+ content: "\FF16";
+}
+.mdi-heart-half::before {
+ content: "\F6DE";
+}
+.mdi-heart-half-full::before {
+ content: "\F6DD";
+}
+.mdi-heart-half-outline::before {
+ content: "\F6DF";
+}
+.mdi-heart-multiple::before {
+ content: "\FA55";
+}
+.mdi-heart-multiple-outline::before {
+ content: "\FA56";
+}
+.mdi-heart-off::before {
+ content: "\F758";
+}
+.mdi-heart-outline::before {
+ content: "\F2D5";
+}
+.mdi-heart-pulse::before {
+ content: "\F5F6";
+}
+.mdi-helicopter::before {
+ content: "\FAC1";
+}
+.mdi-help::before {
+ content: "\F2D6";
+}
+.mdi-help-box::before {
+ content: "\F78A";
+}
+.mdi-help-circle::before {
+ content: "\F2D7";
+}
+.mdi-help-circle-outline::before {
+ content: "\F625";
+}
+.mdi-help-network::before {
+ content: "\F6F4";
+}
+.mdi-help-network-outline::before {
+ content: "\FC66";
+}
+.mdi-help-rhombus::before {
+ content: "\FB81";
+}
+.mdi-help-rhombus-outline::before {
+ content: "\FB82";
+}
+.mdi-hexadecimal::before {
+ content: "\F02D2";
+}
+.mdi-hexagon::before {
+ content: "\F2D8";
+}
+.mdi-hexagon-multiple::before {
+ content: "\F6E0";
+}
+.mdi-hexagon-multiple-outline::before {
+ content: "\F011D";
+}
+.mdi-hexagon-outline::before {
+ content: "\F2D9";
+}
+.mdi-hexagon-slice-1::before {
+ content: "\FAC2";
+}
+.mdi-hexagon-slice-2::before {
+ content: "\FAC3";
+}
+.mdi-hexagon-slice-3::before {
+ content: "\FAC4";
+}
+.mdi-hexagon-slice-4::before {
+ content: "\FAC5";
+}
+.mdi-hexagon-slice-5::before {
+ content: "\FAC6";
+}
+.mdi-hexagon-slice-6::before {
+ content: "\FAC7";
+}
+.mdi-hexagram::before {
+ content: "\FAC8";
+}
+.mdi-hexagram-outline::before {
+ content: "\FAC9";
+}
+.mdi-high-definition::before {
+ content: "\F7CE";
+}
+.mdi-high-definition-box::before {
+ content: "\F877";
+}
+.mdi-highway::before {
+ content: "\F5F7";
+}
+.mdi-hiking::before {
+ content: "\FD5B";
+}
+.mdi-hinduism::before {
+ content: "\F972";
+}
+.mdi-history::before {
+ content: "\F2DA";
+}
+.mdi-hockey-puck::before {
+ content: "\F878";
+}
+.mdi-hockey-sticks::before {
+ content: "\F879";
+}
+.mdi-hololens::before {
+ content: "\F2DB";
+}
+.mdi-home::before {
+ content: "\F2DC";
+}
+.mdi-home-account::before {
+ content: "\F825";
+}
+.mdi-home-alert::before {
+ content: "\F87A";
+}
+.mdi-home-analytics::before {
+ content: "\FED7";
+}
+.mdi-home-assistant::before {
+ content: "\F7CF";
+}
+.mdi-home-automation::before {
+ content: "\F7D0";
+}
+.mdi-home-circle::before {
+ content: "\F7D1";
+}
+.mdi-home-circle-outline::before {
+ content: "\F006F";
+}
+.mdi-home-city::before {
+ content: "\FCF1";
+}
+.mdi-home-city-outline::before {
+ content: "\FCF2";
+}
+.mdi-home-currency-usd::before {
+ content: "\F8AE";
+}
+.mdi-home-edit::before {
+ content: "\F0184";
+}
+.mdi-home-edit-outline::before {
+ content: "\F0185";
+}
+.mdi-home-export-outline::before {
+ content: "\FFB8";
+}
+.mdi-home-flood::before {
+ content: "\FF17";
+}
+.mdi-home-floor-0::before {
+ content: "\FDAE";
+}
+.mdi-home-floor-1::before {
+ content: "\FD5C";
+}
+.mdi-home-floor-2::before {
+ content: "\FD5D";
+}
+.mdi-home-floor-3::before {
+ content: "\FD5E";
+}
+.mdi-home-floor-a::before {
+ content: "\FD5F";
+}
+.mdi-home-floor-b::before {
+ content: "\FD60";
+}
+.mdi-home-floor-g::before {
+ content: "\FD61";
+}
+.mdi-home-floor-l::before {
+ content: "\FD62";
+}
+.mdi-home-floor-negative-1::before {
+ content: "\FDAF";
+}
+.mdi-home-group::before {
+ content: "\FDB0";
+}
+.mdi-home-heart::before {
+ content: "\F826";
+}
+.mdi-home-import-outline::before {
+ content: "\FFB9";
+}
+.mdi-home-lightbulb::before {
+ content: "\F027C";
+}
+.mdi-home-lightbulb-outline::before {
+ content: "\F027D";
+}
+.mdi-home-lock::before {
+ content: "\F8EA";
+}
+.mdi-home-lock-open::before {
+ content: "\F8EB";
+}
+.mdi-home-map-marker::before {
+ content: "\F5F8";
+}
+.mdi-home-minus::before {
+ content: "\F973";
+}
+.mdi-home-modern::before {
+ content: "\F2DD";
+}
+.mdi-home-outline::before {
+ content: "\F6A0";
+}
+.mdi-home-plus::before {
+ content: "\F974";
+}
+.mdi-home-remove::before {
+ content: "\F0272";
+}
+.mdi-home-roof::before {
+ content: "\F0156";
+}
+.mdi-home-thermometer::before {
+ content: "\FF71";
+}
+.mdi-home-thermometer-outline::before {
+ content: "\FF72";
+}
+.mdi-home-variant::before {
+ content: "\F2DE";
+}
+.mdi-home-variant-outline::before {
+ content: "\FB83";
+}
+.mdi-hook::before {
+ content: "\F6E1";
+}
+.mdi-hook-off::before {
+ content: "\F6E2";
+}
+.mdi-hops::before {
+ content: "\F2DF";
+}
+.mdi-horizontal-rotate-clockwise::before {
+ content: "\F011E";
+}
+.mdi-horizontal-rotate-counterclockwise::before {
+ content: "\F011F";
+}
+.mdi-horseshoe::before {
+ content: "\FA57";
+}
+.mdi-hospital::before {
+ content: "\F0017";
+}
+.mdi-hospital-box::before {
+ content: "\F2E0";
+}
+.mdi-hospital-box-outline::before {
+ content: "\F0018";
+}
+.mdi-hospital-building::before {
+ content: "\F2E1";
+}
+.mdi-hospital-marker::before {
+ content: "\F2E2";
+}
+.mdi-hot-tub::before {
+ content: "\F827";
+}
+.mdi-hotel::before {
+ content: "\F2E3";
+}
+.mdi-houzz::before {
+ content: "\F2E4";
+}
+.mdi-houzz-box::before {
+ content: "\F2E5";
+}
+.mdi-hubspot::before {
+ content: "\FCF3";
+}
+.mdi-hulu::before {
+ content: "\F828";
+}
+.mdi-human::before {
+ content: "\F2E6";
+}
+.mdi-human-child::before {
+ content: "\F2E7";
+}
+.mdi-human-female::before {
+ content: "\F649";
+}
+.mdi-human-female-boy::before {
+ content: "\FA58";
+}
+.mdi-human-female-female::before {
+ content: "\FA59";
+}
+.mdi-human-female-girl::before {
+ content: "\FA5A";
+}
+.mdi-human-greeting::before {
+ content: "\F64A";
+}
+.mdi-human-handsdown::before {
+ content: "\F64B";
+}
+.mdi-human-handsup::before {
+ content: "\F64C";
+}
+.mdi-human-male::before {
+ content: "\F64D";
+}
+.mdi-human-male-boy::before {
+ content: "\FA5B";
+}
+.mdi-human-male-female::before {
+ content: "\F2E8";
+}
+.mdi-human-male-girl::before {
+ content: "\FA5C";
+}
+.mdi-human-male-height::before {
+ content: "\FF18";
+}
+.mdi-human-male-height-variant::before {
+ content: "\FF19";
+}
+.mdi-human-male-male::before {
+ content: "\FA5D";
+}
+.mdi-human-pregnant::before {
+ content: "\F5CF";
+}
+.mdi-humble-bundle::before {
+ content: "\F743";
+}
+.mdi-hvac::before {
+ content: "\F037D";
+}
+.mdi-hydraulic-oil-level::before {
+ content: "\F034F";
+}
+.mdi-hydraulic-oil-temperature::before {
+ content: "\F0350";
+}
+.mdi-hydro-power::before {
+ content: "\F0310";
+}
+.mdi-ice-cream::before {
+ content: "\F829";
+}
+.mdi-ice-pop::before {
+ content: "\FF1A";
+}
+.mdi-id-card::before {
+ content: "\FFE0";
+}
+.mdi-identifier::before {
+ content: "\FF1B";
+}
+.mdi-ideogram-cjk::before {
+ content: "\F035C";
+}
+.mdi-ideogram-cjk-variant::before {
+ content: "\F035D";
+}
+.mdi-iframe::before {
+ content: "\FC67";
+}
+.mdi-iframe-array::before {
+ content: "\F0120";
+}
+.mdi-iframe-array-outline::before {
+ content: "\F0121";
+}
+.mdi-iframe-braces::before {
+ content: "\F0122";
+}
+.mdi-iframe-braces-outline::before {
+ content: "\F0123";
+}
+.mdi-iframe-outline::before {
+ content: "\FC68";
+}
+.mdi-iframe-parentheses::before {
+ content: "\F0124";
+}
+.mdi-iframe-parentheses-outline::before {
+ content: "\F0125";
+}
+.mdi-iframe-variable::before {
+ content: "\F0126";
+}
+.mdi-iframe-variable-outline::before {
+ content: "\F0127";
+}
+.mdi-image::before {
+ content: "\F2E9";
+}
+.mdi-image-album::before {
+ content: "\F2EA";
+}
+.mdi-image-area::before {
+ content: "\F2EB";
+}
+.mdi-image-area-close::before {
+ content: "\F2EC";
+}
+.mdi-image-auto-adjust::before {
+ content: "\FFE1";
+}
+.mdi-image-broken::before {
+ content: "\F2ED";
+}
+.mdi-image-broken-variant::before {
+ content: "\F2EE";
+}
+.mdi-image-edit::before {
+ content: "\F020E";
+}
+.mdi-image-edit-outline::before {
+ content: "\F020F";
+}
+.mdi-image-filter::before {
+ content: "\F2EF";
+}
+.mdi-image-filter-black-white::before {
+ content: "\F2F0";
+}
+.mdi-image-filter-center-focus::before {
+ content: "\F2F1";
+}
+.mdi-image-filter-center-focus-strong::before {
+ content: "\FF1C";
+}
+.mdi-image-filter-center-focus-strong-outline::before {
+ content: "\FF1D";
+}
+.mdi-image-filter-center-focus-weak::before {
+ content: "\F2F2";
+}
+.mdi-image-filter-drama::before {
+ content: "\F2F3";
+}
+.mdi-image-filter-frames::before {
+ content: "\F2F4";
+}
+.mdi-image-filter-hdr::before {
+ content: "\F2F5";
+}
+.mdi-image-filter-none::before {
+ content: "\F2F6";
+}
+.mdi-image-filter-tilt-shift::before {
+ content: "\F2F7";
+}
+.mdi-image-filter-vintage::before {
+ content: "\F2F8";
+}
+.mdi-image-frame::before {
+ content: "\FE8A";
+}
+.mdi-image-move::before {
+ content: "\F9F7";
+}
+.mdi-image-multiple::before {
+ content: "\F2F9";
+}
+.mdi-image-off::before {
+ content: "\F82A";
+}
+.mdi-image-off-outline::before {
+ content: "\F01FC";
+}
+.mdi-image-outline::before {
+ content: "\F975";
+}
+.mdi-image-plus::before {
+ content: "\F87B";
+}
+.mdi-image-search::before {
+ content: "\F976";
+}
+.mdi-image-search-outline::before {
+ content: "\F977";
+}
+.mdi-image-size-select-actual::before {
+ content: "\FC69";
+}
+.mdi-image-size-select-large::before {
+ content: "\FC6A";
+}
+.mdi-image-size-select-small::before {
+ content: "\FC6B";
+}
+.mdi-import::before {
+ content: "\F2FA";
+}
+.mdi-inbox::before {
+ content: "\F686";
+}
+.mdi-inbox-arrow-down::before {
+ content: "\F2FB";
+}
+.mdi-inbox-arrow-down-outline::before {
+ content: "\F029B";
+}
+.mdi-inbox-arrow-up::before {
+ content: "\F3D1";
+}
+.mdi-inbox-arrow-up-outline::before {
+ content: "\F029C";
+}
+.mdi-inbox-full::before {
+ content: "\F029D";
+}
+.mdi-inbox-full-outline::before {
+ content: "\F029E";
+}
+.mdi-inbox-multiple::before {
+ content: "\F8AF";
+}
+.mdi-inbox-multiple-outline::before {
+ content: "\FB84";
+}
+.mdi-inbox-outline::before {
+ content: "\F029F";
+}
+.mdi-incognito::before {
+ content: "\F5F9";
+}
+.mdi-infinity::before {
+ content: "\F6E3";
+}
+.mdi-information::before {
+ content: "\F2FC";
+}
+.mdi-information-outline::before {
+ content: "\F2FD";
+}
+.mdi-information-variant::before {
+ content: "\F64E";
+}
+.mdi-instagram::before {
+ content: "\F2FE";
+}
+.mdi-instapaper::before {
+ content: "\F2FF";
+}
+.mdi-instrument-triangle::before {
+ content: "\F0070";
+}
+.mdi-internet-explorer::before {
+ content: "\F300";
+}
+.mdi-invert-colors::before {
+ content: "\F301";
+}
+.mdi-invert-colors-off::before {
+ content: "\FE8B";
+}
+.mdi-iobroker::before {
+ content: "\F0313";
+}
+.mdi-ip::before {
+ content: "\FA5E";
+}
+.mdi-ip-network::before {
+ content: "\FA5F";
+}
+.mdi-ip-network-outline::before {
+ content: "\FC6C";
+}
+.mdi-ipod::before {
+ content: "\FC6D";
+}
+.mdi-islam::before {
+ content: "\F978";
+}
+.mdi-island::before {
+ content: "\F0071";
+}
+.mdi-itunes::before {
+ content: "\F676";
+}
+.mdi-iv-bag::before {
+ content: "\F00E4";
+}
+.mdi-jabber::before {
+ content: "\FDB1";
+}
+.mdi-jeepney::before {
+ content: "\F302";
+}
+.mdi-jellyfish::before {
+ content: "\FF1E";
+}
+.mdi-jellyfish-outline::before {
+ content: "\FF1F";
+}
+.mdi-jira::before {
+ content: "\F303";
+}
+.mdi-jquery::before {
+ content: "\F87C";
+}
+.mdi-jsfiddle::before {
+ content: "\F304";
+}
+.mdi-json::before {
+ content: "\F626";
+}
+.mdi-judaism::before {
+ content: "\F979";
+}
+.mdi-jump-rope::before {
+ content: "\F032A";
+}
+.mdi-kabaddi::before {
+ content: "\FD63";
+}
+.mdi-karate::before {
+ content: "\F82B";
+}
+.mdi-keg::before {
+ content: "\F305";
+}
+.mdi-kettle::before {
+ content: "\F5FA";
+}
+.mdi-kettle-alert::before {
+ content: "\F0342";
+}
+.mdi-kettle-alert-outline::before {
+ content: "\F0343";
+}
+.mdi-kettle-off::before {
+ content: "\F0346";
+}
+.mdi-kettle-off-outline::before {
+ content: "\F0347";
+}
+.mdi-kettle-outline::before {
+ content: "\FF73";
+}
+.mdi-kettle-steam::before {
+ content: "\F0344";
+}
+.mdi-kettle-steam-outline::before {
+ content: "\F0345";
+}
+.mdi-kettlebell::before {
+ content: "\F032B";
+}
+.mdi-key::before {
+ content: "\F306";
+}
+.mdi-key-arrow-right::before {
+ content: "\F033D";
+}
+.mdi-key-change::before {
+ content: "\F307";
+}
+.mdi-key-link::before {
+ content: "\F01CA";
+}
+.mdi-key-minus::before {
+ content: "\F308";
+}
+.mdi-key-outline::before {
+ content: "\FDB2";
+}
+.mdi-key-plus::before {
+ content: "\F309";
+}
+.mdi-key-remove::before {
+ content: "\F30A";
+}
+.mdi-key-star::before {
+ content: "\F01C9";
+}
+.mdi-key-variant::before {
+ content: "\F30B";
+}
+.mdi-key-wireless::before {
+ content: "\FFE2";
+}
+.mdi-keyboard::before {
+ content: "\F30C";
+}
+.mdi-keyboard-backspace::before {
+ content: "\F30D";
+}
+.mdi-keyboard-caps::before {
+ content: "\F30E";
+}
+.mdi-keyboard-close::before {
+ content: "\F30F";
+}
+.mdi-keyboard-esc::before {
+ content: "\F02E2";
+}
+.mdi-keyboard-f1::before {
+ content: "\F02D6";
+}
+.mdi-keyboard-f10::before {
+ content: "\F02DF";
+}
+.mdi-keyboard-f11::before {
+ content: "\F02E0";
+}
+.mdi-keyboard-f12::before {
+ content: "\F02E1";
+}
+.mdi-keyboard-f2::before {
+ content: "\F02D7";
+}
+.mdi-keyboard-f3::before {
+ content: "\F02D8";
+}
+.mdi-keyboard-f4::before {
+ content: "\F02D9";
+}
+.mdi-keyboard-f5::before {
+ content: "\F02DA";
+}
+.mdi-keyboard-f6::before {
+ content: "\F02DB";
+}
+.mdi-keyboard-f7::before {
+ content: "\F02DC";
+}
+.mdi-keyboard-f8::before {
+ content: "\F02DD";
+}
+.mdi-keyboard-f9::before {
+ content: "\F02DE";
+}
+.mdi-keyboard-off::before {
+ content: "\F310";
+}
+.mdi-keyboard-off-outline::before {
+ content: "\FE8C";
+}
+.mdi-keyboard-outline::before {
+ content: "\F97A";
+}
+.mdi-keyboard-return::before {
+ content: "\F311";
+}
+.mdi-keyboard-settings::before {
+ content: "\F9F8";
+}
+.mdi-keyboard-settings-outline::before {
+ content: "\F9F9";
+}
+.mdi-keyboard-space::before {
+ content: "\F0072";
+}
+.mdi-keyboard-tab::before {
+ content: "\F312";
+}
+.mdi-keyboard-variant::before {
+ content: "\F313";
+}
+.mdi-khanda::before {
+ content: "\F0128";
+}
+.mdi-kickstarter::before {
+ content: "\F744";
+}
+.mdi-klingon::before {
+ content: "\F0386";
+}
+.mdi-knife::before {
+ content: "\F9FA";
+}
+.mdi-knife-military::before {
+ content: "\F9FB";
+}
+.mdi-kodi::before {
+ content: "\F314";
+}
+.mdi-kotlin::before {
+ content: "\F0244";
+}
+.mdi-kubernetes::before {
+ content: "\F0129";
+}
+.mdi-label::before {
+ content: "\F315";
+}
+.mdi-label-multiple::before {
+ content: "\F03A0";
+}
+.mdi-label-multiple-outline::before {
+ content: "\F03A1";
+}
+.mdi-label-off::before {
+ content: "\FACA";
+}
+.mdi-label-off-outline::before {
+ content: "\FACB";
+}
+.mdi-label-outline::before {
+ content: "\F316";
+}
+.mdi-label-percent::before {
+ content: "\F0315";
+}
+.mdi-label-percent-outline::before {
+ content: "\F0316";
+}
+.mdi-label-variant::before {
+ content: "\FACC";
+}
+.mdi-label-variant-outline::before {
+ content: "\FACD";
+}
+.mdi-ladybug::before {
+ content: "\F82C";
+}
+.mdi-lambda::before {
+ content: "\F627";
+}
+.mdi-lamp::before {
+ content: "\F6B4";
+}
+.mdi-lan::before {
+ content: "\F317";
+}
+.mdi-lan-check::before {
+ content: "\F02D5";
+}
+.mdi-lan-connect::before {
+ content: "\F318";
+}
+.mdi-lan-disconnect::before {
+ content: "\F319";
+}
+.mdi-lan-pending::before {
+ content: "\F31A";
+}
+.mdi-language-c::before {
+ content: "\F671";
+}
+.mdi-language-cpp::before {
+ content: "\F672";
+}
+.mdi-language-csharp::before {
+ content: "\F31B";
+}
+.mdi-language-css3::before {
+ content: "\F31C";
+}
+.mdi-language-fortran::before {
+ content: "\F0245";
+}
+.mdi-language-go::before {
+ content: "\F7D2";
+}
+.mdi-language-haskell::before {
+ content: "\FC6E";
+}
+.mdi-language-html5::before {
+ content: "\F31D";
+}
+.mdi-language-java::before {
+ content: "\FB1C";
+}
+.mdi-language-javascript::before {
+ content: "\F31E";
+}
+.mdi-language-lua::before {
+ content: "\F8B0";
+}
+.mdi-language-php::before {
+ content: "\F31F";
+}
+.mdi-language-python::before {
+ content: "\F320";
+}
+.mdi-language-python-text::before {
+ content: "\F321";
+}
+.mdi-language-r::before {
+ content: "\F7D3";
+}
+.mdi-language-ruby-on-rails::before {
+ content: "\FACE";
+}
+.mdi-language-swift::before {
+ content: "\F6E4";
+}
+.mdi-language-typescript::before {
+ content: "\F6E5";
+}
+.mdi-laptop::before {
+ content: "\F322";
+}
+.mdi-laptop-chromebook::before {
+ content: "\F323";
+}
+.mdi-laptop-mac::before {
+ content: "\F324";
+}
+.mdi-laptop-off::before {
+ content: "\F6E6";
+}
+.mdi-laptop-windows::before {
+ content: "\F325";
+}
+.mdi-laravel::before {
+ content: "\FACF";
+}
+.mdi-lasso::before {
+ content: "\FF20";
+}
+.mdi-lastfm::before {
+ content: "\F326";
+}
+.mdi-lastpass::before {
+ content: "\F446";
+}
+.mdi-latitude::before {
+ content: "\FF74";
+}
+.mdi-launch::before {
+ content: "\F327";
+}
+.mdi-lava-lamp::before {
+ content: "\F7D4";
+}
+.mdi-layers::before {
+ content: "\F328";
+}
+.mdi-layers-minus::before {
+ content: "\FE8D";
+}
+.mdi-layers-off::before {
+ content: "\F329";
+}
+.mdi-layers-off-outline::before {
+ content: "\F9FC";
+}
+.mdi-layers-outline::before {
+ content: "\F9FD";
+}
+.mdi-layers-plus::before {
+ content: "\FE30";
+}
+.mdi-layers-remove::before {
+ content: "\FE31";
+}
+.mdi-layers-search::before {
+ content: "\F0231";
+}
+.mdi-layers-search-outline::before {
+ content: "\F0232";
+}
+.mdi-layers-triple::before {
+ content: "\FF75";
+}
+.mdi-layers-triple-outline::before {
+ content: "\FF76";
+}
+.mdi-lead-pencil::before {
+ content: "\F64F";
+}
+.mdi-leaf::before {
+ content: "\F32A";
+}
+.mdi-leaf-maple::before {
+ content: "\FC6F";
+}
+.mdi-leaf-maple-off::before {
+ content: "\F0305";
+}
+.mdi-leaf-off::before {
+ content: "\F0304";
+}
+.mdi-leak::before {
+ content: "\FDB3";
+}
+.mdi-leak-off::before {
+ content: "\FDB4";
+}
+.mdi-led-off::before {
+ content: "\F32B";
+}
+.mdi-led-on::before {
+ content: "\F32C";
+}
+.mdi-led-outline::before {
+ content: "\F32D";
+}
+.mdi-led-strip::before {
+ content: "\F7D5";
+}
+.mdi-led-strip-variant::before {
+ content: "\F0073";
+}
+.mdi-led-variant-off::before {
+ content: "\F32E";
+}
+.mdi-led-variant-on::before {
+ content: "\F32F";
+}
+.mdi-led-variant-outline::before {
+ content: "\F330";
+}
+.mdi-leek::before {
+ content: "\F01A8";
+}
+.mdi-less-than::before {
+ content: "\F97B";
+}
+.mdi-less-than-or-equal::before {
+ content: "\F97C";
+}
+.mdi-library::before {
+ content: "\F331";
+}
+.mdi-library-books::before {
+ content: "\F332";
+}
+.mdi-library-movie::before {
+ content: "\FCF4";
+}
+.mdi-library-music::before {
+ content: "\F333";
+}
+.mdi-library-music-outline::before {
+ content: "\FF21";
+}
+.mdi-library-shelves::before {
+ content: "\FB85";
+}
+.mdi-library-video::before {
+ content: "\FCF5";
+}
+.mdi-license::before {
+ content: "\FFE3";
+}
+.mdi-lifebuoy::before {
+ content: "\F87D";
+}
+.mdi-light-switch::before {
+ content: "\F97D";
+}
+.mdi-lightbulb::before {
+ content: "\F335";
+}
+.mdi-lightbulb-cfl::before {
+ content: "\F0233";
+}
+.mdi-lightbulb-cfl-off::before {
+ content: "\F0234";
+}
+.mdi-lightbulb-cfl-spiral::before {
+ content: "\F02A0";
+}
+.mdi-lightbulb-cfl-spiral-off::before {
+ content: "\F02EE";
+}
+.mdi-lightbulb-group::before {
+ content: "\F027E";
+}
+.mdi-lightbulb-group-off::before {
+ content: "\F02F8";
+}
+.mdi-lightbulb-group-off-outline::before {
+ content: "\F02F9";
+}
+.mdi-lightbulb-group-outline::before {
+ content: "\F027F";
+}
+.mdi-lightbulb-multiple::before {
+ content: "\F0280";
+}
+.mdi-lightbulb-multiple-off::before {
+ content: "\F02FA";
+}
+.mdi-lightbulb-multiple-off-outline::before {
+ content: "\F02FB";
+}
+.mdi-lightbulb-multiple-outline::before {
+ content: "\F0281";
+}
+.mdi-lightbulb-off::before {
+ content: "\FE32";
+}
+.mdi-lightbulb-off-outline::before {
+ content: "\FE33";
+}
+.mdi-lightbulb-on::before {
+ content: "\F6E7";
+}
+.mdi-lightbulb-on-outline::before {
+ content: "\F6E8";
+}
+.mdi-lightbulb-outline::before {
+ content: "\F336";
+}
+.mdi-lighthouse::before {
+ content: "\F9FE";
+}
+.mdi-lighthouse-on::before {
+ content: "\F9FF";
+}
+.mdi-link::before {
+ content: "\F337";
+}
+.mdi-link-box::before {
+ content: "\FCF6";
+}
+.mdi-link-box-outline::before {
+ content: "\FCF7";
+}
+.mdi-link-box-variant::before {
+ content: "\FCF8";
+}
+.mdi-link-box-variant-outline::before {
+ content: "\FCF9";
+}
+.mdi-link-lock::before {
+ content: "\F00E5";
+}
+.mdi-link-off::before {
+ content: "\F338";
+}
+.mdi-link-plus::before {
+ content: "\FC70";
+}
+.mdi-link-variant::before {
+ content: "\F339";
+}
+.mdi-link-variant-minus::before {
+ content: "\F012A";
+}
+.mdi-link-variant-off::before {
+ content: "\F33A";
+}
+.mdi-link-variant-plus::before {
+ content: "\F012B";
+}
+.mdi-link-variant-remove::before {
+ content: "\F012C";
+}
+.mdi-linkedin::before {
+ content: "\F33B";
+}
+.mdi-linkedin-box::before {
+ content: "\F33C";
+}
+.mdi-linux::before {
+ content: "\F33D";
+}
+.mdi-linux-mint::before {
+ content: "\F8EC";
+}
+.mdi-litecoin::before {
+ content: "\FA60";
+}
+.mdi-loading::before {
+ content: "\F771";
+}
+.mdi-location-enter::before {
+ content: "\FFE4";
+}
+.mdi-location-exit::before {
+ content: "\FFE5";
+}
+.mdi-lock::before {
+ content: "\F33E";
+}
+.mdi-lock-alert::before {
+ content: "\F8ED";
+}
+.mdi-lock-clock::before {
+ content: "\F97E";
+}
+.mdi-lock-open::before {
+ content: "\F33F";
+}
+.mdi-lock-open-outline::before {
+ content: "\F340";
+}
+.mdi-lock-open-variant::before {
+ content: "\FFE6";
+}
+.mdi-lock-open-variant-outline::before {
+ content: "\FFE7";
+}
+.mdi-lock-outline::before {
+ content: "\F341";
+}
+.mdi-lock-pattern::before {
+ content: "\F6E9";
+}
+.mdi-lock-plus::before {
+ content: "\F5FB";
+}
+.mdi-lock-question::before {
+ content: "\F8EE";
+}
+.mdi-lock-reset::before {
+ content: "\F772";
+}
+.mdi-lock-smart::before {
+ content: "\F8B1";
+}
+.mdi-locker::before {
+ content: "\F7D6";
+}
+.mdi-locker-multiple::before {
+ content: "\F7D7";
+}
+.mdi-login::before {
+ content: "\F342";
+}
+.mdi-login-variant::before {
+ content: "\F5FC";
+}
+.mdi-logout::before {
+ content: "\F343";
+}
+.mdi-logout-variant::before {
+ content: "\F5FD";
+}
+.mdi-longitude::before {
+ content: "\FF77";
+}
+.mdi-looks::before {
+ content: "\F344";
+}
+.mdi-loupe::before {
+ content: "\F345";
+}
+.mdi-lumx::before {
+ content: "\F346";
+}
+.mdi-lungs::before {
+ content: "\F00AF";
+}
+.mdi-lyft::before {
+ content: "\FB1D";
+}
+.mdi-magnet::before {
+ content: "\F347";
+}
+.mdi-magnet-on::before {
+ content: "\F348";
+}
+.mdi-magnify::before {
+ content: "\F349";
+}
+.mdi-magnify-close::before {
+ content: "\F97F";
+}
+.mdi-magnify-minus::before {
+ content: "\F34A";
+}
+.mdi-magnify-minus-cursor::before {
+ content: "\FA61";
+}
+.mdi-magnify-minus-outline::before {
+ content: "\F6EB";
+}
+.mdi-magnify-plus::before {
+ content: "\F34B";
+}
+.mdi-magnify-plus-cursor::before {
+ content: "\FA62";
+}
+.mdi-magnify-plus-outline::before {
+ content: "\F6EC";
+}
+.mdi-magnify-remove-cursor::before {
+ content: "\F0237";
+}
+.mdi-magnify-remove-outline::before {
+ content: "\F0238";
+}
+.mdi-magnify-scan::before {
+ content: "\F02A1";
+}
+.mdi-mail::before {
+ content: "\FED8";
+}
+.mdi-mail-ru::before {
+ content: "\F34C";
+}
+.mdi-mailbox::before {
+ content: "\F6ED";
+}
+.mdi-mailbox-open::before {
+ content: "\FD64";
+}
+.mdi-mailbox-open-outline::before {
+ content: "\FD65";
+}
+.mdi-mailbox-open-up::before {
+ content: "\FD66";
+}
+.mdi-mailbox-open-up-outline::before {
+ content: "\FD67";
+}
+.mdi-mailbox-outline::before {
+ content: "\FD68";
+}
+.mdi-mailbox-up::before {
+ content: "\FD69";
+}
+.mdi-mailbox-up-outline::before {
+ content: "\FD6A";
+}
+.mdi-map::before {
+ content: "\F34D";
+}
+.mdi-map-check::before {
+ content: "\FED9";
+}
+.mdi-map-check-outline::before {
+ content: "\FEDA";
+}
+.mdi-map-clock::before {
+ content: "\FCFA";
+}
+.mdi-map-clock-outline::before {
+ content: "\FCFB";
+}
+.mdi-map-legend::before {
+ content: "\FA00";
+}
+.mdi-map-marker::before {
+ content: "\F34E";
+}
+.mdi-map-marker-alert::before {
+ content: "\FF22";
+}
+.mdi-map-marker-alert-outline::before {
+ content: "\FF23";
+}
+.mdi-map-marker-check::before {
+ content: "\FC71";
+}
+.mdi-map-marker-check-outline::before {
+ content: "\F0326";
+}
+.mdi-map-marker-circle::before {
+ content: "\F34F";
+}
+.mdi-map-marker-distance::before {
+ content: "\F8EF";
+}
+.mdi-map-marker-down::before {
+ content: "\F012D";
+}
+.mdi-map-marker-left::before {
+ content: "\F0306";
+}
+.mdi-map-marker-left-outline::before {
+ content: "\F0308";
+}
+.mdi-map-marker-minus::before {
+ content: "\F650";
+}
+.mdi-map-marker-minus-outline::before {
+ content: "\F0324";
+}
+.mdi-map-marker-multiple::before {
+ content: "\F350";
+}
+.mdi-map-marker-multiple-outline::before {
+ content: "\F02A2";
+}
+.mdi-map-marker-off::before {
+ content: "\F351";
+}
+.mdi-map-marker-off-outline::before {
+ content: "\F0328";
+}
+.mdi-map-marker-outline::before {
+ content: "\F7D8";
+}
+.mdi-map-marker-path::before {
+ content: "\FCFC";
+}
+.mdi-map-marker-plus::before {
+ content: "\F651";
+}
+.mdi-map-marker-plus-outline::before {
+ content: "\F0323";
+}
+.mdi-map-marker-question::before {
+ content: "\FF24";
+}
+.mdi-map-marker-question-outline::before {
+ content: "\FF25";
+}
+.mdi-map-marker-radius::before {
+ content: "\F352";
+}
+.mdi-map-marker-radius-outline::before {
+ content: "\F0327";
+}
+.mdi-map-marker-remove::before {
+ content: "\FF26";
+}
+.mdi-map-marker-remove-outline::before {
+ content: "\F0325";
+}
+.mdi-map-marker-remove-variant::before {
+ content: "\FF27";
+}
+.mdi-map-marker-right::before {
+ content: "\F0307";
+}
+.mdi-map-marker-right-outline::before {
+ content: "\F0309";
+}
+.mdi-map-marker-up::before {
+ content: "\F012E";
+}
+.mdi-map-minus::before {
+ content: "\F980";
+}
+.mdi-map-outline::before {
+ content: "\F981";
+}
+.mdi-map-plus::before {
+ content: "\F982";
+}
+.mdi-map-search::before {
+ content: "\F983";
+}
+.mdi-map-search-outline::before {
+ content: "\F984";
+}
+.mdi-mapbox::before {
+ content: "\FB86";
+}
+.mdi-margin::before {
+ content: "\F353";
+}
+.mdi-markdown::before {
+ content: "\F354";
+}
+.mdi-markdown-outline::before {
+ content: "\FF78";
+}
+.mdi-marker::before {
+ content: "\F652";
+}
+.mdi-marker-cancel::before {
+ content: "\FDB5";
+}
+.mdi-marker-check::before {
+ content: "\F355";
+}
+.mdi-mastodon::before {
+ content: "\FAD0";
+}
+.mdi-mastodon-variant::before {
+ content: "\FAD1";
+}
+.mdi-material-design::before {
+ content: "\F985";
+}
+.mdi-material-ui::before {
+ content: "\F357";
+}
+.mdi-math-compass::before {
+ content: "\F358";
+}
+.mdi-math-cos::before {
+ content: "\FC72";
+}
+.mdi-math-integral::before {
+ content: "\FFE8";
+}
+.mdi-math-integral-box::before {
+ content: "\FFE9";
+}
+.mdi-math-log::before {
+ content: "\F00B0";
+}
+.mdi-math-norm::before {
+ content: "\FFEA";
+}
+.mdi-math-norm-box::before {
+ content: "\FFEB";
+}
+.mdi-math-sin::before {
+ content: "\FC73";
+}
+.mdi-math-tan::before {
+ content: "\FC74";
+}
+.mdi-matrix::before {
+ content: "\F628";
+}
+.mdi-medal::before {
+ content: "\F986";
+}
+.mdi-medal-outline::before {
+ content: "\F0351";
+}
+.mdi-medical-bag::before {
+ content: "\F6EE";
+}
+.mdi-meditation::before {
+ content: "\F01A6";
+}
+.mdi-medium::before {
+ content: "\F35A";
+}
+.mdi-meetup::before {
+ content: "\FAD2";
+}
+.mdi-memory::before {
+ content: "\F35B";
+}
+.mdi-menu::before {
+ content: "\F35C";
+}
+.mdi-menu-down::before {
+ content: "\F35D";
+}
+.mdi-menu-down-outline::before {
+ content: "\F6B5";
+}
+.mdi-menu-left::before {
+ content: "\F35E";
+}
+.mdi-menu-left-outline::before {
+ content: "\FA01";
+}
+.mdi-menu-open::before {
+ content: "\FB87";
+}
+.mdi-menu-right::before {
+ content: "\F35F";
+}
+.mdi-menu-right-outline::before {
+ content: "\FA02";
+}
+.mdi-menu-swap::before {
+ content: "\FA63";
+}
+.mdi-menu-swap-outline::before {
+ content: "\FA64";
+}
+.mdi-menu-up::before {
+ content: "\F360";
+}
+.mdi-menu-up-outline::before {
+ content: "\F6B6";
+}
+.mdi-merge::before {
+ content: "\FF79";
+}
+.mdi-message::before {
+ content: "\F361";
+}
+.mdi-message-alert::before {
+ content: "\F362";
+}
+.mdi-message-alert-outline::before {
+ content: "\FA03";
+}
+.mdi-message-arrow-left::before {
+ content: "\F031D";
+}
+.mdi-message-arrow-left-outline::before {
+ content: "\F031E";
+}
+.mdi-message-arrow-right::before {
+ content: "\F031F";
+}
+.mdi-message-arrow-right-outline::before {
+ content: "\F0320";
+}
+.mdi-message-bulleted::before {
+ content: "\F6A1";
+}
+.mdi-message-bulleted-off::before {
+ content: "\F6A2";
+}
+.mdi-message-draw::before {
+ content: "\F363";
+}
+.mdi-message-image::before {
+ content: "\F364";
+}
+.mdi-message-image-outline::before {
+ content: "\F0197";
+}
+.mdi-message-lock::before {
+ content: "\FFEC";
+}
+.mdi-message-lock-outline::before {
+ content: "\F0198";
+}
+.mdi-message-minus::before {
+ content: "\F0199";
+}
+.mdi-message-minus-outline::before {
+ content: "\F019A";
+}
+.mdi-message-outline::before {
+ content: "\F365";
+}
+.mdi-message-plus::before {
+ content: "\F653";
+}
+.mdi-message-plus-outline::before {
+ content: "\F00E6";
+}
+.mdi-message-processing::before {
+ content: "\F366";
+}
+.mdi-message-processing-outline::before {
+ content: "\F019B";
+}
+.mdi-message-reply::before {
+ content: "\F367";
+}
+.mdi-message-reply-text::before {
+ content: "\F368";
+}
+.mdi-message-settings::before {
+ content: "\F6EF";
+}
+.mdi-message-settings-outline::before {
+ content: "\F019C";
+}
+.mdi-message-settings-variant::before {
+ content: "\F6F0";
+}
+.mdi-message-settings-variant-outline::before {
+ content: "\F019D";
+}
+.mdi-message-text::before {
+ content: "\F369";
+}
+.mdi-message-text-clock::before {
+ content: "\F019E";
+}
+.mdi-message-text-clock-outline::before {
+ content: "\F019F";
+}
+.mdi-message-text-lock::before {
+ content: "\FFED";
+}
+.mdi-message-text-lock-outline::before {
+ content: "\F01A0";
+}
+.mdi-message-text-outline::before {
+ content: "\F36A";
+}
+.mdi-message-video::before {
+ content: "\F36B";
+}
+.mdi-meteor::before {
+ content: "\F629";
+}
+.mdi-metronome::before {
+ content: "\F7D9";
+}
+.mdi-metronome-tick::before {
+ content: "\F7DA";
+}
+.mdi-micro-sd::before {
+ content: "\F7DB";
+}
+.mdi-microphone::before {
+ content: "\F36C";
+}
+.mdi-microphone-minus::before {
+ content: "\F8B2";
+}
+.mdi-microphone-off::before {
+ content: "\F36D";
+}
+.mdi-microphone-outline::before {
+ content: "\F36E";
+}
+.mdi-microphone-plus::before {
+ content: "\F8B3";
+}
+.mdi-microphone-settings::before {
+ content: "\F36F";
+}
+.mdi-microphone-variant::before {
+ content: "\F370";
+}
+.mdi-microphone-variant-off::before {
+ content: "\F371";
+}
+.mdi-microscope::before {
+ content: "\F654";
+}
+.mdi-microsoft::before {
+ content: "\F372";
+}
+.mdi-microsoft-dynamics::before {
+ content: "\F987";
+}
+.mdi-microwave::before {
+ content: "\FC75";
+}
+.mdi-middleware::before {
+ content: "\FF7A";
+}
+.mdi-middleware-outline::before {
+ content: "\FF7B";
+}
+.mdi-midi::before {
+ content: "\F8F0";
+}
+.mdi-midi-port::before {
+ content: "\F8F1";
+}
+.mdi-mine::before {
+ content: "\FDB6";
+}
+.mdi-minecraft::before {
+ content: "\F373";
+}
+.mdi-mini-sd::before {
+ content: "\FA04";
+}
+.mdi-minidisc::before {
+ content: "\FA05";
+}
+.mdi-minus::before {
+ content: "\F374";
+}
+.mdi-minus-box::before {
+ content: "\F375";
+}
+.mdi-minus-box-multiple::before {
+ content: "\F016C";
+}
+.mdi-minus-box-multiple-outline::before {
+ content: "\F016D";
+}
+.mdi-minus-box-outline::before {
+ content: "\F6F1";
+}
+.mdi-minus-circle::before {
+ content: "\F376";
+}
+.mdi-minus-circle-outline::before {
+ content: "\F377";
+}
+.mdi-minus-network::before {
+ content: "\F378";
+}
+.mdi-minus-network-outline::before {
+ content: "\FC76";
+}
+.mdi-mirror::before {
+ content: "\F0228";
+}
+.mdi-mixcloud::before {
+ content: "\F62A";
+}
+.mdi-mixed-martial-arts::before {
+ content: "\FD6B";
+}
+.mdi-mixed-reality::before {
+ content: "\F87E";
+}
+.mdi-mixer::before {
+ content: "\F7DC";
+}
+.mdi-molecule::before {
+ content: "\FB88";
+}
+.mdi-monitor::before {
+ content: "\F379";
+}
+.mdi-monitor-cellphone::before {
+ content: "\F988";
+}
+.mdi-monitor-cellphone-star::before {
+ content: "\F989";
+}
+.mdi-monitor-clean::before {
+ content: "\F012F";
+}
+.mdi-monitor-dashboard::before {
+ content: "\FA06";
+}
+.mdi-monitor-edit::before {
+ content: "\F02F1";
+}
+.mdi-monitor-lock::before {
+ content: "\FDB7";
+}
+.mdi-monitor-multiple::before {
+ content: "\F37A";
+}
+.mdi-monitor-off::before {
+ content: "\FD6C";
+}
+.mdi-monitor-screenshot::before {
+ content: "\FE34";
+}
+.mdi-monitor-speaker::before {
+ content: "\FF7C";
+}
+.mdi-monitor-speaker-off::before {
+ content: "\FF7D";
+}
+.mdi-monitor-star::before {
+ content: "\FDB8";
+}
+.mdi-moon-first-quarter::before {
+ content: "\FF7E";
+}
+.mdi-moon-full::before {
+ content: "\FF7F";
+}
+.mdi-moon-last-quarter::before {
+ content: "\FF80";
+}
+.mdi-moon-new::before {
+ content: "\FF81";
+}
+.mdi-moon-waning-crescent::before {
+ content: "\FF82";
+}
+.mdi-moon-waning-gibbous::before {
+ content: "\FF83";
+}
+.mdi-moon-waxing-crescent::before {
+ content: "\FF84";
+}
+.mdi-moon-waxing-gibbous::before {
+ content: "\FF85";
+}
+.mdi-moped::before {
+ content: "\F00B1";
+}
+.mdi-more::before {
+ content: "\F37B";
+}
+.mdi-mother-heart::before {
+ content: "\F033F";
+}
+.mdi-mother-nurse::before {
+ content: "\FCFD";
+}
+.mdi-motion-sensor::before {
+ content: "\FD6D";
+}
+.mdi-motorbike::before {
+ content: "\F37C";
+}
+.mdi-mouse::before {
+ content: "\F37D";
+}
+.mdi-mouse-bluetooth::before {
+ content: "\F98A";
+}
+.mdi-mouse-off::before {
+ content: "\F37E";
+}
+.mdi-mouse-variant::before {
+ content: "\F37F";
+}
+.mdi-mouse-variant-off::before {
+ content: "\F380";
+}
+.mdi-move-resize::before {
+ content: "\F655";
+}
+.mdi-move-resize-variant::before {
+ content: "\F656";
+}
+.mdi-movie::before {
+ content: "\F381";
+}
+.mdi-movie-edit::before {
+ content: "\F014D";
+}
+.mdi-movie-edit-outline::before {
+ content: "\F014E";
+}
+.mdi-movie-filter::before {
+ content: "\F014F";
+}
+.mdi-movie-filter-outline::before {
+ content: "\F0150";
+}
+.mdi-movie-open::before {
+ content: "\FFEE";
+}
+.mdi-movie-open-outline::before {
+ content: "\FFEF";
+}
+.mdi-movie-outline::before {
+ content: "\FDB9";
+}
+.mdi-movie-roll::before {
+ content: "\F7DD";
+}
+.mdi-movie-search::before {
+ content: "\F01FD";
+}
+.mdi-movie-search-outline::before {
+ content: "\F01FE";
+}
+.mdi-muffin::before {
+ content: "\F98B";
+}
+.mdi-multiplication::before {
+ content: "\F382";
+}
+.mdi-multiplication-box::before {
+ content: "\F383";
+}
+.mdi-mushroom::before {
+ content: "\F7DE";
+}
+.mdi-mushroom-outline::before {
+ content: "\F7DF";
+}
+.mdi-music::before {
+ content: "\F759";
+}
+.mdi-music-accidental-double-flat::before {
+ content: "\FF86";
+}
+.mdi-music-accidental-double-sharp::before {
+ content: "\FF87";
+}
+.mdi-music-accidental-flat::before {
+ content: "\FF88";
+}
+.mdi-music-accidental-natural::before {
+ content: "\FF89";
+}
+.mdi-music-accidental-sharp::before {
+ content: "\FF8A";
+}
+.mdi-music-box::before {
+ content: "\F384";
+}
+.mdi-music-box-outline::before {
+ content: "\F385";
+}
+.mdi-music-circle::before {
+ content: "\F386";
+}
+.mdi-music-circle-outline::before {
+ content: "\FAD3";
+}
+.mdi-music-clef-alto::before {
+ content: "\FF8B";
+}
+.mdi-music-clef-bass::before {
+ content: "\FF8C";
+}
+.mdi-music-clef-treble::before {
+ content: "\FF8D";
+}
+.mdi-music-note::before {
+ content: "\F387";
+}
+.mdi-music-note-bluetooth::before {
+ content: "\F5FE";
+}
+.mdi-music-note-bluetooth-off::before {
+ content: "\F5FF";
+}
+.mdi-music-note-eighth::before {
+ content: "\F388";
+}
+.mdi-music-note-eighth-dotted::before {
+ content: "\FF8E";
+}
+.mdi-music-note-half::before {
+ content: "\F389";
+}
+.mdi-music-note-half-dotted::before {
+ content: "\FF8F";
+}
+.mdi-music-note-off::before {
+ content: "\F38A";
+}
+.mdi-music-note-off-outline::before {
+ content: "\FF90";
+}
+.mdi-music-note-outline::before {
+ content: "\FF91";
+}
+.mdi-music-note-plus::before {
+ content: "\FDBA";
+}
+.mdi-music-note-quarter::before {
+ content: "\F38B";
+}
+.mdi-music-note-quarter-dotted::before {
+ content: "\FF92";
+}
+.mdi-music-note-sixteenth::before {
+ content: "\F38C";
+}
+.mdi-music-note-sixteenth-dotted::before {
+ content: "\FF93";
+}
+.mdi-music-note-whole::before {
+ content: "\F38D";
+}
+.mdi-music-note-whole-dotted::before {
+ content: "\FF94";
+}
+.mdi-music-off::before {
+ content: "\F75A";
+}
+.mdi-music-rest-eighth::before {
+ content: "\FF95";
+}
+.mdi-music-rest-half::before {
+ content: "\FF96";
+}
+.mdi-music-rest-quarter::before {
+ content: "\FF97";
+}
+.mdi-music-rest-sixteenth::before {
+ content: "\FF98";
+}
+.mdi-music-rest-whole::before {
+ content: "\FF99";
+}
+.mdi-nail::before {
+ content: "\FDBB";
+}
+.mdi-nas::before {
+ content: "\F8F2";
+}
+.mdi-nativescript::before {
+ content: "\F87F";
+}
+.mdi-nature::before {
+ content: "\F38E";
+}
+.mdi-nature-people::before {
+ content: "\F38F";
+}
+.mdi-navigation::before {
+ content: "\F390";
+}
+.mdi-near-me::before {
+ content: "\F5CD";
+}
+.mdi-necklace::before {
+ content: "\FF28";
+}
+.mdi-needle::before {
+ content: "\F391";
+}
+.mdi-netflix::before {
+ content: "\F745";
+}
+.mdi-network::before {
+ content: "\F6F2";
+}
+.mdi-network-off::before {
+ content: "\FC77";
+}
+.mdi-network-off-outline::before {
+ content: "\FC78";
+}
+.mdi-network-outline::before {
+ content: "\FC79";
+}
+.mdi-network-router::before {
+ content: "\F00B2";
+}
+.mdi-network-strength-1::before {
+ content: "\F8F3";
+}
+.mdi-network-strength-1-alert::before {
+ content: "\F8F4";
+}
+.mdi-network-strength-2::before {
+ content: "\F8F5";
+}
+.mdi-network-strength-2-alert::before {
+ content: "\F8F6";
+}
+.mdi-network-strength-3::before {
+ content: "\F8F7";
+}
+.mdi-network-strength-3-alert::before {
+ content: "\F8F8";
+}
+.mdi-network-strength-4::before {
+ content: "\F8F9";
+}
+.mdi-network-strength-4-alert::before {
+ content: "\F8FA";
+}
+.mdi-network-strength-off::before {
+ content: "\F8FB";
+}
+.mdi-network-strength-off-outline::before {
+ content: "\F8FC";
+}
+.mdi-network-strength-outline::before {
+ content: "\F8FD";
+}
+.mdi-new-box::before {
+ content: "\F394";
+}
+.mdi-newspaper::before {
+ content: "\F395";
+}
+.mdi-newspaper-minus::before {
+ content: "\FF29";
+}
+.mdi-newspaper-plus::before {
+ content: "\FF2A";
+}
+.mdi-newspaper-variant::before {
+ content: "\F0023";
+}
+.mdi-newspaper-variant-multiple::before {
+ content: "\F0024";
+}
+.mdi-newspaper-variant-multiple-outline::before {
+ content: "\F0025";
+}
+.mdi-newspaper-variant-outline::before {
+ content: "\F0026";
+}
+.mdi-nfc::before {
+ content: "\F396";
+}
+.mdi-nfc-off::before {
+ content: "\FE35";
+}
+.mdi-nfc-search-variant::before {
+ content: "\FE36";
+}
+.mdi-nfc-tap::before {
+ content: "\F397";
+}
+.mdi-nfc-variant::before {
+ content: "\F398";
+}
+.mdi-nfc-variant-off::before {
+ content: "\FE37";
+}
+.mdi-ninja::before {
+ content: "\F773";
+}
+.mdi-nintendo-switch::before {
+ content: "\F7E0";
+}
+.mdi-nix::before {
+ content: "\F0130";
+}
+.mdi-nodejs::before {
+ content: "\F399";
+}
+.mdi-noodles::before {
+ content: "\F01A9";
+}
+.mdi-not-equal::before {
+ content: "\F98C";
+}
+.mdi-not-equal-variant::before {
+ content: "\F98D";
+}
+.mdi-note::before {
+ content: "\F39A";
+}
+.mdi-note-multiple::before {
+ content: "\F6B7";
+}
+.mdi-note-multiple-outline::before {
+ content: "\F6B8";
+}
+.mdi-note-outline::before {
+ content: "\F39B";
+}
+.mdi-note-plus::before {
+ content: "\F39C";
+}
+.mdi-note-plus-outline::before {
+ content: "\F39D";
+}
+.mdi-note-text::before {
+ content: "\F39E";
+}
+.mdi-note-text-outline::before {
+ content: "\F0202";
+}
+.mdi-notebook::before {
+ content: "\F82D";
+}
+.mdi-notebook-multiple::before {
+ content: "\FE38";
+}
+.mdi-notebook-outline::before {
+ content: "\FEDC";
+}
+.mdi-notification-clear-all::before {
+ content: "\F39F";
+}
+.mdi-npm::before {
+ content: "\F6F6";
+}
+.mdi-npm-variant::before {
+ content: "\F98E";
+}
+.mdi-npm-variant-outline::before {
+ content: "\F98F";
+}
+.mdi-nuke::before {
+ content: "\F6A3";
+}
+.mdi-null::before {
+ content: "\F7E1";
+}
+.mdi-numeric::before {
+ content: "\F3A0";
+}
+.mdi-numeric-0::before {
+ content: "\30";
+}
+.mdi-numeric-0-box::before {
+ content: "\F3A1";
+}
+.mdi-numeric-0-box-multiple::before {
+ content: "\FF2B";
+}
+.mdi-numeric-0-box-multiple-outline::before {
+ content: "\F3A2";
+}
+.mdi-numeric-0-box-outline::before {
+ content: "\F3A3";
+}
+.mdi-numeric-0-circle::before {
+ content: "\FC7A";
+}
+.mdi-numeric-0-circle-outline::before {
+ content: "\FC7B";
+}
+.mdi-numeric-1::before {
+ content: "\31";
+}
+.mdi-numeric-1-box::before {
+ content: "\F3A4";
+}
+.mdi-numeric-1-box-multiple::before {
+ content: "\FF2C";
+}
+.mdi-numeric-1-box-multiple-outline::before {
+ content: "\F3A5";
+}
+.mdi-numeric-1-box-outline::before {
+ content: "\F3A6";
+}
+.mdi-numeric-1-circle::before {
+ content: "\FC7C";
+}
+.mdi-numeric-1-circle-outline::before {
+ content: "\FC7D";
+}
+.mdi-numeric-10::before {
+ content: "\F000A";
+}
+.mdi-numeric-10-box::before {
+ content: "\FF9A";
+}
+.mdi-numeric-10-box-multiple::before {
+ content: "\F000B";
+}
+.mdi-numeric-10-box-multiple-outline::before {
+ content: "\F000C";
+}
+.mdi-numeric-10-box-outline::before {
+ content: "\FF9B";
+}
+.mdi-numeric-10-circle::before {
+ content: "\F000D";
+}
+.mdi-numeric-10-circle-outline::before {
+ content: "\F000E";
+}
+.mdi-numeric-2::before {
+ content: "\32";
+}
+.mdi-numeric-2-box::before {
+ content: "\F3A7";
+}
+.mdi-numeric-2-box-multiple::before {
+ content: "\FF2D";
+}
+.mdi-numeric-2-box-multiple-outline::before {
+ content: "\F3A8";
+}
+.mdi-numeric-2-box-outline::before {
+ content: "\F3A9";
+}
+.mdi-numeric-2-circle::before {
+ content: "\FC7E";
+}
+.mdi-numeric-2-circle-outline::before {
+ content: "\FC7F";
+}
+.mdi-numeric-3::before {
+ content: "\33";
+}
+.mdi-numeric-3-box::before {
+ content: "\F3AA";
+}
+.mdi-numeric-3-box-multiple::before {
+ content: "\FF2E";
+}
+.mdi-numeric-3-box-multiple-outline::before {
+ content: "\F3AB";
+}
+.mdi-numeric-3-box-outline::before {
+ content: "\F3AC";
+}
+.mdi-numeric-3-circle::before {
+ content: "\FC80";
+}
+.mdi-numeric-3-circle-outline::before {
+ content: "\FC81";
+}
+.mdi-numeric-4::before {
+ content: "\34";
+}
+.mdi-numeric-4-box::before {
+ content: "\F3AD";
+}
+.mdi-numeric-4-box-multiple::before {
+ content: "\FF2F";
+}
+.mdi-numeric-4-box-multiple-outline::before {
+ content: "\F3AE";
+}
+.mdi-numeric-4-box-outline::before {
+ content: "\F3AF";
+}
+.mdi-numeric-4-circle::before {
+ content: "\FC82";
+}
+.mdi-numeric-4-circle-outline::before {
+ content: "\FC83";
+}
+.mdi-numeric-5::before {
+ content: "\35";
+}
+.mdi-numeric-5-box::before {
+ content: "\F3B0";
+}
+.mdi-numeric-5-box-multiple::before {
+ content: "\FF30";
+}
+.mdi-numeric-5-box-multiple-outline::before {
+ content: "\F3B1";
+}
+.mdi-numeric-5-box-outline::before {
+ content: "\F3B2";
+}
+.mdi-numeric-5-circle::before {
+ content: "\FC84";
+}
+.mdi-numeric-5-circle-outline::before {
+ content: "\FC85";
+}
+.mdi-numeric-6::before {
+ content: "\36";
+}
+.mdi-numeric-6-box::before {
+ content: "\F3B3";
+}
+.mdi-numeric-6-box-multiple::before {
+ content: "\FF31";
+}
+.mdi-numeric-6-box-multiple-outline::before {
+ content: "\F3B4";
+}
+.mdi-numeric-6-box-outline::before {
+ content: "\F3B5";
+}
+.mdi-numeric-6-circle::before {
+ content: "\FC86";
+}
+.mdi-numeric-6-circle-outline::before {
+ content: "\FC87";
+}
+.mdi-numeric-7::before {
+ content: "\37";
+}
+.mdi-numeric-7-box::before {
+ content: "\F3B6";
+}
+.mdi-numeric-7-box-multiple::before {
+ content: "\FF32";
+}
+.mdi-numeric-7-box-multiple-outline::before {
+ content: "\F3B7";
+}
+.mdi-numeric-7-box-outline::before {
+ content: "\F3B8";
+}
+.mdi-numeric-7-circle::before {
+ content: "\FC88";
+}
+.mdi-numeric-7-circle-outline::before {
+ content: "\FC89";
+}
+.mdi-numeric-8::before {
+ content: "\38";
+}
+.mdi-numeric-8-box::before {
+ content: "\F3B9";
+}
+.mdi-numeric-8-box-multiple::before {
+ content: "\FF33";
+}
+.mdi-numeric-8-box-multiple-outline::before {
+ content: "\F3BA";
+}
+.mdi-numeric-8-box-outline::before {
+ content: "\F3BB";
+}
+.mdi-numeric-8-circle::before {
+ content: "\FC8A";
+}
+.mdi-numeric-8-circle-outline::before {
+ content: "\FC8B";
+}
+.mdi-numeric-9::before {
+ content: "\39";
+}
+.mdi-numeric-9-box::before {
+ content: "\F3BC";
+}
+.mdi-numeric-9-box-multiple::before {
+ content: "\FF34";
+}
+.mdi-numeric-9-box-multiple-outline::before {
+ content: "\F3BD";
+}
+.mdi-numeric-9-box-outline::before {
+ content: "\F3BE";
+}
+.mdi-numeric-9-circle::before {
+ content: "\FC8C";
+}
+.mdi-numeric-9-circle-outline::before {
+ content: "\FC8D";
+}
+.mdi-numeric-9-plus::before {
+ content: "\F000F";
+}
+.mdi-numeric-9-plus-box::before {
+ content: "\F3BF";
+}
+.mdi-numeric-9-plus-box-multiple::before {
+ content: "\FF35";
+}
+.mdi-numeric-9-plus-box-multiple-outline::before {
+ content: "\F3C0";
+}
+.mdi-numeric-9-plus-box-outline::before {
+ content: "\F3C1";
+}
+.mdi-numeric-9-plus-circle::before {
+ content: "\FC8E";
+}
+.mdi-numeric-9-plus-circle-outline::before {
+ content: "\FC8F";
+}
+.mdi-numeric-negative-1::before {
+ content: "\F0074";
+}
+.mdi-nut::before {
+ content: "\F6F7";
+}
+.mdi-nutrition::before {
+ content: "\F3C2";
+}
+.mdi-nuxt::before {
+ content: "\F0131";
+}
+.mdi-oar::before {
+ content: "\F67B";
+}
+.mdi-ocarina::before {
+ content: "\FDBC";
+}
+.mdi-oci::before {
+ content: "\F0314";
+}
+.mdi-ocr::before {
+ content: "\F0165";
+}
+.mdi-octagon::before {
+ content: "\F3C3";
+}
+.mdi-octagon-outline::before {
+ content: "\F3C4";
+}
+.mdi-octagram::before {
+ content: "\F6F8";
+}
+.mdi-octagram-outline::before {
+ content: "\F774";
+}
+.mdi-odnoklassniki::before {
+ content: "\F3C5";
+}
+.mdi-offer::before {
+ content: "\F0246";
+}
+.mdi-office::before {
+ content: "\F3C6";
+}
+.mdi-office-building::before {
+ content: "\F990";
+}
+.mdi-oil::before {
+ content: "\F3C7";
+}
+.mdi-oil-lamp::before {
+ content: "\FF36";
+}
+.mdi-oil-level::before {
+ content: "\F0075";
+}
+.mdi-oil-temperature::before {
+ content: "\F0019";
+}
+.mdi-omega::before {
+ content: "\F3C9";
+}
+.mdi-one-up::before {
+ content: "\FB89";
+}
+.mdi-onedrive::before {
+ content: "\F3CA";
+}
+.mdi-onenote::before {
+ content: "\F746";
+}
+.mdi-onepassword::before {
+ content: "\F880";
+}
+.mdi-opacity::before {
+ content: "\F5CC";
+}
+.mdi-open-in-app::before {
+ content: "\F3CB";
+}
+.mdi-open-in-new::before {
+ content: "\F3CC";
+}
+.mdi-open-source-initiative::before {
+ content: "\FB8A";
+}
+.mdi-openid::before {
+ content: "\F3CD";
+}
+.mdi-opera::before {
+ content: "\F3CE";
+}
+.mdi-orbit::before {
+ content: "\F018";
+}
+.mdi-origin::before {
+ content: "\FB2B";
+}
+.mdi-ornament::before {
+ content: "\F3CF";
+}
+.mdi-ornament-variant::before {
+ content: "\F3D0";
+}
+.mdi-outdoor-lamp::before {
+ content: "\F0076";
+}
+.mdi-outlook::before {
+ content: "\FCFE";
+}
+.mdi-overscan::before {
+ content: "\F0027";
+}
+.mdi-owl::before {
+ content: "\F3D2";
+}
+.mdi-pac-man::before {
+ content: "\FB8B";
+}
+.mdi-package::before {
+ content: "\F3D3";
+}
+.mdi-package-down::before {
+ content: "\F3D4";
+}
+.mdi-package-up::before {
+ content: "\F3D5";
+}
+.mdi-package-variant::before {
+ content: "\F3D6";
+}
+.mdi-package-variant-closed::before {
+ content: "\F3D7";
+}
+.mdi-page-first::before {
+ content: "\F600";
+}
+.mdi-page-last::before {
+ content: "\F601";
+}
+.mdi-page-layout-body::before {
+ content: "\F6F9";
+}
+.mdi-page-layout-footer::before {
+ content: "\F6FA";
+}
+.mdi-page-layout-header::before {
+ content: "\F6FB";
+}
+.mdi-page-layout-header-footer::before {
+ content: "\FF9C";
+}
+.mdi-page-layout-sidebar-left::before {
+ content: "\F6FC";
+}
+.mdi-page-layout-sidebar-right::before {
+ content: "\F6FD";
+}
+.mdi-page-next::before {
+ content: "\FB8C";
+}
+.mdi-page-next-outline::before {
+ content: "\FB8D";
+}
+.mdi-page-previous::before {
+ content: "\FB8E";
+}
+.mdi-page-previous-outline::before {
+ content: "\FB8F";
+}
+.mdi-palette::before {
+ content: "\F3D8";
+}
+.mdi-palette-advanced::before {
+ content: "\F3D9";
+}
+.mdi-palette-outline::before {
+ content: "\FE6C";
+}
+.mdi-palette-swatch::before {
+ content: "\F8B4";
+}
+.mdi-palette-swatch-outline::before {
+ content: "\F0387";
+}
+.mdi-palm-tree::before {
+ content: "\F0077";
+}
+.mdi-pan::before {
+ content: "\FB90";
+}
+.mdi-pan-bottom-left::before {
+ content: "\FB91";
+}
+.mdi-pan-bottom-right::before {
+ content: "\FB92";
+}
+.mdi-pan-down::before {
+ content: "\FB93";
+}
+.mdi-pan-horizontal::before {
+ content: "\FB94";
+}
+.mdi-pan-left::before {
+ content: "\FB95";
+}
+.mdi-pan-right::before {
+ content: "\FB96";
+}
+.mdi-pan-top-left::before {
+ content: "\FB97";
+}
+.mdi-pan-top-right::before {
+ content: "\FB98";
+}
+.mdi-pan-up::before {
+ content: "\FB99";
+}
+.mdi-pan-vertical::before {
+ content: "\FB9A";
+}
+.mdi-panda::before {
+ content: "\F3DA";
+}
+.mdi-pandora::before {
+ content: "\F3DB";
+}
+.mdi-panorama::before {
+ content: "\F3DC";
+}
+.mdi-panorama-fisheye::before {
+ content: "\F3DD";
+}
+.mdi-panorama-horizontal::before {
+ content: "\F3DE";
+}
+.mdi-panorama-vertical::before {
+ content: "\F3DF";
+}
+.mdi-panorama-wide-angle::before {
+ content: "\F3E0";
+}
+.mdi-paper-cut-vertical::before {
+ content: "\F3E1";
+}
+.mdi-paper-roll::before {
+ content: "\F0182";
+}
+.mdi-paper-roll-outline::before {
+ content: "\F0183";
+}
+.mdi-paperclip::before {
+ content: "\F3E2";
+}
+.mdi-parachute::before {
+ content: "\FC90";
+}
+.mdi-parachute-outline::before {
+ content: "\FC91";
+}
+.mdi-parking::before {
+ content: "\F3E3";
+}
+.mdi-party-popper::before {
+ content: "\F0078";
+}
+.mdi-passport::before {
+ content: "\F7E2";
+}
+.mdi-passport-biometric::before {
+ content: "\FDBD";
+}
+.mdi-pasta::before {
+ content: "\F018B";
+}
+.mdi-patio-heater::before {
+ content: "\FF9D";
+}
+.mdi-patreon::before {
+ content: "\F881";
+}
+.mdi-pause::before {
+ content: "\F3E4";
+}
+.mdi-pause-circle::before {
+ content: "\F3E5";
+}
+.mdi-pause-circle-outline::before {
+ content: "\F3E6";
+}
+.mdi-pause-octagon::before {
+ content: "\F3E7";
+}
+.mdi-pause-octagon-outline::before {
+ content: "\F3E8";
+}
+.mdi-paw::before {
+ content: "\F3E9";
+}
+.mdi-paw-off::before {
+ content: "\F657";
+}
+.mdi-paypal::before {
+ content: "\F882";
+}
+.mdi-pdf-box::before {
+ content: "\FE39";
+}
+.mdi-peace::before {
+ content: "\F883";
+}
+.mdi-peanut::before {
+ content: "\F001E";
+}
+.mdi-peanut-off::before {
+ content: "\F001F";
+}
+.mdi-peanut-off-outline::before {
+ content: "\F0021";
+}
+.mdi-peanut-outline::before {
+ content: "\F0020";
+}
+.mdi-pen::before {
+ content: "\F3EA";
+}
+.mdi-pen-lock::before {
+ content: "\FDBE";
+}
+.mdi-pen-minus::before {
+ content: "\FDBF";
+}
+.mdi-pen-off::before {
+ content: "\FDC0";
+}
+.mdi-pen-plus::before {
+ content: "\FDC1";
+}
+.mdi-pen-remove::before {
+ content: "\FDC2";
+}
+.mdi-pencil::before {
+ content: "\F3EB";
+}
+.mdi-pencil-box::before {
+ content: "\F3EC";
+}
+.mdi-pencil-box-multiple::before {
+ content: "\F016F";
+}
+.mdi-pencil-box-multiple-outline::before {
+ content: "\F0170";
+}
+.mdi-pencil-box-outline::before {
+ content: "\F3ED";
+}
+.mdi-pencil-circle::before {
+ content: "\F6FE";
+}
+.mdi-pencil-circle-outline::before {
+ content: "\F775";
+}
+.mdi-pencil-lock::before {
+ content: "\F3EE";
+}
+.mdi-pencil-lock-outline::before {
+ content: "\FDC3";
+}
+.mdi-pencil-minus::before {
+ content: "\FDC4";
+}
+.mdi-pencil-minus-outline::before {
+ content: "\FDC5";
+}
+.mdi-pencil-off::before {
+ content: "\F3EF";
+}
+.mdi-pencil-off-outline::before {
+ content: "\FDC6";
+}
+.mdi-pencil-outline::before {
+ content: "\FC92";
+}
+.mdi-pencil-plus::before {
+ content: "\FDC7";
+}
+.mdi-pencil-plus-outline::before {
+ content: "\FDC8";
+}
+.mdi-pencil-remove::before {
+ content: "\FDC9";
+}
+.mdi-pencil-remove-outline::before {
+ content: "\FDCA";
+}
+.mdi-pencil-ruler::before {
+ content: "\F037E";
+}
+.mdi-penguin::before {
+ content: "\FEDD";
+}
+.mdi-pentagon::before {
+ content: "\F6FF";
+}
+.mdi-pentagon-outline::before {
+ content: "\F700";
+}
+.mdi-percent::before {
+ content: "\F3F0";
+}
+.mdi-percent-outline::before {
+ content: "\F02A3";
+}
+.mdi-periodic-table::before {
+ content: "\F8B5";
+}
+.mdi-periodic-table-co::before {
+ content: "\F0329";
+}
+.mdi-periodic-table-co2::before {
+ content: "\F7E3";
+}
+.mdi-periscope::before {
+ content: "\F747";
+}
+.mdi-perspective-less::before {
+ content: "\FCFF";
+}
+.mdi-perspective-more::before {
+ content: "\FD00";
+}
+.mdi-pharmacy::before {
+ content: "\F3F1";
+}
+.mdi-phone::before {
+ content: "\F3F2";
+}
+.mdi-phone-alert::before {
+ content: "\FF37";
+}
+.mdi-phone-alert-outline::before {
+ content: "\F01B9";
+}
+.mdi-phone-bluetooth::before {
+ content: "\F3F3";
+}
+.mdi-phone-bluetooth-outline::before {
+ content: "\F01BA";
+}
+.mdi-phone-cancel::before {
+ content: "\F00E7";
+}
+.mdi-phone-cancel-outline::before {
+ content: "\F01BB";
+}
+.mdi-phone-check::before {
+ content: "\F01D4";
+}
+.mdi-phone-check-outline::before {
+ content: "\F01D5";
+}
+.mdi-phone-classic::before {
+ content: "\F602";
+}
+.mdi-phone-classic-off::before {
+ content: "\F02A4";
+}
+.mdi-phone-forward::before {
+ content: "\F3F4";
+}
+.mdi-phone-forward-outline::before {
+ content: "\F01BC";
+}
+.mdi-phone-hangup::before {
+ content: "\F3F5";
+}
+.mdi-phone-hangup-outline::before {
+ content: "\F01BD";
+}
+.mdi-phone-in-talk::before {
+ content: "\F3F6";
+}
+.mdi-phone-in-talk-outline::before {
+ content: "\F01AD";
+}
+.mdi-phone-incoming::before {
+ content: "\F3F7";
+}
+.mdi-phone-incoming-outline::before {
+ content: "\F01BE";
+}
+.mdi-phone-lock::before {
+ content: "\F3F8";
+}
+.mdi-phone-lock-outline::before {
+ content: "\F01BF";
+}
+.mdi-phone-log::before {
+ content: "\F3F9";
+}
+.mdi-phone-log-outline::before {
+ content: "\F01C0";
+}
+.mdi-phone-message::before {
+ content: "\F01C1";
+}
+.mdi-phone-message-outline::before {
+ content: "\F01C2";
+}
+.mdi-phone-minus::before {
+ content: "\F658";
+}
+.mdi-phone-minus-outline::before {
+ content: "\F01C3";
+}
+.mdi-phone-missed::before {
+ content: "\F3FA";
+}
+.mdi-phone-missed-outline::before {
+ content: "\F01D0";
+}
+.mdi-phone-off::before {
+ content: "\FDCB";
+}
+.mdi-phone-off-outline::before {
+ content: "\F01D1";
+}
+.mdi-phone-outgoing::before {
+ content: "\F3FB";
+}
+.mdi-phone-outgoing-outline::before {
+ content: "\F01C4";
+}
+.mdi-phone-outline::before {
+ content: "\FDCC";
+}
+.mdi-phone-paused::before {
+ content: "\F3FC";
+}
+.mdi-phone-paused-outline::before {
+ content: "\F01C5";
+}
+.mdi-phone-plus::before {
+ content: "\F659";
+}
+.mdi-phone-plus-outline::before {
+ content: "\F01C6";
+}
+.mdi-phone-return::before {
+ content: "\F82E";
+}
+.mdi-phone-return-outline::before {
+ content: "\F01C7";
+}
+.mdi-phone-ring::before {
+ content: "\F01D6";
+}
+.mdi-phone-ring-outline::before {
+ content: "\F01D7";
+}
+.mdi-phone-rotate-landscape::before {
+ content: "\F884";
+}
+.mdi-phone-rotate-portrait::before {
+ content: "\F885";
+}
+.mdi-phone-settings::before {
+ content: "\F3FD";
+}
+.mdi-phone-settings-outline::before {
+ content: "\F01C8";
+}
+.mdi-phone-voip::before {
+ content: "\F3FE";
+}
+.mdi-pi::before {
+ content: "\F3FF";
+}
+.mdi-pi-box::before {
+ content: "\F400";
+}
+.mdi-pi-hole::before {
+ content: "\FDCD";
+}
+.mdi-piano::before {
+ content: "\F67C";
+}
+.mdi-pickaxe::before {
+ content: "\F8B6";
+}
+.mdi-picture-in-picture-bottom-right::before {
+ content: "\FE3A";
+}
+.mdi-picture-in-picture-bottom-right-outline::before {
+ content: "\FE3B";
+}
+.mdi-picture-in-picture-top-right::before {
+ content: "\FE3C";
+}
+.mdi-picture-in-picture-top-right-outline::before {
+ content: "\FE3D";
+}
+.mdi-pier::before {
+ content: "\F886";
+}
+.mdi-pier-crane::before {
+ content: "\F887";
+}
+.mdi-pig::before {
+ content: "\F401";
+}
+.mdi-pig-variant::before {
+ content: "\F0028";
+}
+.mdi-piggy-bank::before {
+ content: "\F0029";
+}
+.mdi-pill::before {
+ content: "\F402";
+}
+.mdi-pillar::before {
+ content: "\F701";
+}
+.mdi-pin::before {
+ content: "\F403";
+}
+.mdi-pin-off::before {
+ content: "\F404";
+}
+.mdi-pin-off-outline::before {
+ content: "\F92F";
+}
+.mdi-pin-outline::before {
+ content: "\F930";
+}
+.mdi-pine-tree::before {
+ content: "\F405";
+}
+.mdi-pine-tree-box::before {
+ content: "\F406";
+}
+.mdi-pinterest::before {
+ content: "\F407";
+}
+.mdi-pinterest-box::before {
+ content: "\F408";
+}
+.mdi-pinwheel::before {
+ content: "\FAD4";
+}
+.mdi-pinwheel-outline::before {
+ content: "\FAD5";
+}
+.mdi-pipe::before {
+ content: "\F7E4";
+}
+.mdi-pipe-disconnected::before {
+ content: "\F7E5";
+}
+.mdi-pipe-leak::before {
+ content: "\F888";
+}
+.mdi-pipe-wrench::before {
+ content: "\F037F";
+}
+.mdi-pirate::before {
+ content: "\FA07";
+}
+.mdi-pistol::before {
+ content: "\F702";
+}
+.mdi-piston::before {
+ content: "\F889";
+}
+.mdi-pizza::before {
+ content: "\F409";
+}
+.mdi-play::before {
+ content: "\F40A";
+}
+.mdi-play-box::before {
+ content: "\F02A5";
+}
+.mdi-play-box-outline::before {
+ content: "\F40B";
+}
+.mdi-play-circle::before {
+ content: "\F40C";
+}
+.mdi-play-circle-outline::before {
+ content: "\F40D";
+}
+.mdi-play-network::before {
+ content: "\F88A";
+}
+.mdi-play-network-outline::before {
+ content: "\FC93";
+}
+.mdi-play-outline::before {
+ content: "\FF38";
+}
+.mdi-play-pause::before {
+ content: "\F40E";
+}
+.mdi-play-protected-content::before {
+ content: "\F40F";
+}
+.mdi-play-speed::before {
+ content: "\F8FE";
+}
+.mdi-playlist-check::before {
+ content: "\F5C7";
+}
+.mdi-playlist-edit::before {
+ content: "\F8FF";
+}
+.mdi-playlist-minus::before {
+ content: "\F410";
+}
+.mdi-playlist-music::before {
+ content: "\FC94";
+}
+.mdi-playlist-music-outline::before {
+ content: "\FC95";
+}
+.mdi-playlist-play::before {
+ content: "\F411";
+}
+.mdi-playlist-plus::before {
+ content: "\F412";
+}
+.mdi-playlist-remove::before {
+ content: "\F413";
+}
+.mdi-playlist-star::before {
+ content: "\FDCE";
+}
+.mdi-playstation::before {
+ content: "\F414";
+}
+.mdi-plex::before {
+ content: "\F6B9";
+}
+.mdi-plus::before {
+ content: "\F415";
+}
+.mdi-plus-box::before {
+ content: "\F416";
+}
+.mdi-plus-box-multiple::before {
+ content: "\F334";
+}
+.mdi-plus-box-multiple-outline::before {
+ content: "\F016E";
+}
+.mdi-plus-box-outline::before {
+ content: "\F703";
+}
+.mdi-plus-circle::before {
+ content: "\F417";
+}
+.mdi-plus-circle-multiple-outline::before {
+ content: "\F418";
+}
+.mdi-plus-circle-outline::before {
+ content: "\F419";
+}
+.mdi-plus-minus::before {
+ content: "\F991";
+}
+.mdi-plus-minus-box::before {
+ content: "\F992";
+}
+.mdi-plus-network::before {
+ content: "\F41A";
+}
+.mdi-plus-network-outline::before {
+ content: "\FC96";
+}
+.mdi-plus-one::before {
+ content: "\F41B";
+}
+.mdi-plus-outline::before {
+ content: "\F704";
+}
+.mdi-plus-thick::before {
+ content: "\F0217";
+}
+.mdi-pocket::before {
+ content: "\F41C";
+}
+.mdi-podcast::before {
+ content: "\F993";
+}
+.mdi-podium::before {
+ content: "\FD01";
+}
+.mdi-podium-bronze::before {
+ content: "\FD02";
+}
+.mdi-podium-gold::before {
+ content: "\FD03";
+}
+.mdi-podium-silver::before {
+ content: "\FD04";
+}
+.mdi-point-of-sale::before {
+ content: "\FD6E";
+}
+.mdi-pokeball::before {
+ content: "\F41D";
+}
+.mdi-pokemon-go::before {
+ content: "\FA08";
+}
+.mdi-poker-chip::before {
+ content: "\F82F";
+}
+.mdi-polaroid::before {
+ content: "\F41E";
+}
+.mdi-police-badge::before {
+ content: "\F0192";
+}
+.mdi-police-badge-outline::before {
+ content: "\F0193";
+}
+.mdi-poll::before {
+ content: "\F41F";
+}
+.mdi-poll-box::before {
+ content: "\F420";
+}
+.mdi-poll-box-outline::before {
+ content: "\F02A6";
+}
+.mdi-polymer::before {
+ content: "\F421";
+}
+.mdi-pool::before {
+ content: "\F606";
+}
+.mdi-popcorn::before {
+ content: "\F422";
+}
+.mdi-post::before {
+ content: "\F002A";
+}
+.mdi-post-outline::before {
+ content: "\F002B";
+}
+.mdi-postage-stamp::before {
+ content: "\FC97";
+}
+.mdi-pot::before {
+ content: "\F65A";
+}
+.mdi-pot-mix::before {
+ content: "\F65B";
+}
+.mdi-pound::before {
+ content: "\F423";
+}
+.mdi-pound-box::before {
+ content: "\F424";
+}
+.mdi-pound-box-outline::before {
+ content: "\F01AA";
+}
+.mdi-power::before {
+ content: "\F425";
+}
+.mdi-power-cycle::before {
+ content: "\F900";
+}
+.mdi-power-off::before {
+ content: "\F901";
+}
+.mdi-power-on::before {
+ content: "\F902";
+}
+.mdi-power-plug::before {
+ content: "\F6A4";
+}
+.mdi-power-plug-off::before {
+ content: "\F6A5";
+}
+.mdi-power-settings::before {
+ content: "\F426";
+}
+.mdi-power-sleep::before {
+ content: "\F903";
+}
+.mdi-power-socket::before {
+ content: "\F427";
+}
+.mdi-power-socket-au::before {
+ content: "\F904";
+}
+.mdi-power-socket-de::before {
+ content: "\F0132";
+}
+.mdi-power-socket-eu::before {
+ content: "\F7E6";
+}
+.mdi-power-socket-fr::before {
+ content: "\F0133";
+}
+.mdi-power-socket-jp::before {
+ content: "\F0134";
+}
+.mdi-power-socket-uk::before {
+ content: "\F7E7";
+}
+.mdi-power-socket-us::before {
+ content: "\F7E8";
+}
+.mdi-power-standby::before {
+ content: "\F905";
+}
+.mdi-powershell::before {
+ content: "\FA09";
+}
+.mdi-prescription::before {
+ content: "\F705";
+}
+.mdi-presentation::before {
+ content: "\F428";
+}
+.mdi-presentation-play::before {
+ content: "\F429";
+}
+.mdi-printer::before {
+ content: "\F42A";
+}
+.mdi-printer-3d::before {
+ content: "\F42B";
+}
+.mdi-printer-3d-nozzle::before {
+ content: "\FE3E";
+}
+.mdi-printer-3d-nozzle-alert::before {
+ content: "\F01EB";
+}
+.mdi-printer-3d-nozzle-alert-outline::before {
+ content: "\F01EC";
+}
+.mdi-printer-3d-nozzle-outline::before {
+ content: "\FE3F";
+}
+.mdi-printer-alert::before {
+ content: "\F42C";
+}
+.mdi-printer-check::before {
+ content: "\F0171";
+}
+.mdi-printer-off::before {
+ content: "\FE40";
+}
+.mdi-printer-pos::before {
+ content: "\F0079";
+}
+.mdi-printer-settings::before {
+ content: "\F706";
+}
+.mdi-printer-wireless::before {
+ content: "\FA0A";
+}
+.mdi-priority-high::before {
+ content: "\F603";
+}
+.mdi-priority-low::before {
+ content: "\F604";
+}
+.mdi-professional-hexagon::before {
+ content: "\F42D";
+}
+.mdi-progress-alert::before {
+ content: "\FC98";
+}
+.mdi-progress-check::before {
+ content: "\F994";
+}
+.mdi-progress-clock::before {
+ content: "\F995";
+}
+.mdi-progress-close::before {
+ content: "\F0135";
+}
+.mdi-progress-download::before {
+ content: "\F996";
+}
+.mdi-progress-upload::before {
+ content: "\F997";
+}
+.mdi-progress-wrench::before {
+ content: "\FC99";
+}
+.mdi-projector::before {
+ content: "\F42E";
+}
+.mdi-projector-screen::before {
+ content: "\F42F";
+}
+.mdi-propane-tank::before {
+ content: "\F0382";
+}
+.mdi-propane-tank-outline::before {
+ content: "\F0383";
+}
+.mdi-protocol::before {
+ content: "\FFF9";
+}
+.mdi-publish::before {
+ content: "\F6A6";
+}
+.mdi-pulse::before {
+ content: "\F430";
+}
+.mdi-pumpkin::before {
+ content: "\FB9B";
+}
+.mdi-purse::before {
+ content: "\FF39";
+}
+.mdi-purse-outline::before {
+ content: "\FF3A";
+}
+.mdi-puzzle::before {
+ content: "\F431";
+}
+.mdi-puzzle-outline::before {
+ content: "\FA65";
+}
+.mdi-qi::before {
+ content: "\F998";
+}
+.mdi-qqchat::before {
+ content: "\F605";
+}
+.mdi-qrcode::before {
+ content: "\F432";
+}
+.mdi-qrcode-edit::before {
+ content: "\F8B7";
+}
+.mdi-qrcode-minus::before {
+ content: "\F01B7";
+}
+.mdi-qrcode-plus::before {
+ content: "\F01B6";
+}
+.mdi-qrcode-remove::before {
+ content: "\F01B8";
+}
+.mdi-qrcode-scan::before {
+ content: "\F433";
+}
+.mdi-quadcopter::before {
+ content: "\F434";
+}
+.mdi-quality-high::before {
+ content: "\F435";
+}
+.mdi-quality-low::before {
+ content: "\FA0B";
+}
+.mdi-quality-medium::before {
+ content: "\FA0C";
+}
+.mdi-quicktime::before {
+ content: "\F436";
+}
+.mdi-quora::before {
+ content: "\FD05";
+}
+.mdi-rabbit::before {
+ content: "\F906";
+}
+.mdi-racing-helmet::before {
+ content: "\FD6F";
+}
+.mdi-racquetball::before {
+ content: "\FD70";
+}
+.mdi-radar::before {
+ content: "\F437";
+}
+.mdi-radiator::before {
+ content: "\F438";
+}
+.mdi-radiator-disabled::before {
+ content: "\FAD6";
+}
+.mdi-radiator-off::before {
+ content: "\FAD7";
+}
+.mdi-radio::before {
+ content: "\F439";
+}
+.mdi-radio-am::before {
+ content: "\FC9A";
+}
+.mdi-radio-fm::before {
+ content: "\FC9B";
+}
+.mdi-radio-handheld::before {
+ content: "\F43A";
+}
+.mdi-radio-off::before {
+ content: "\F0247";
+}
+.mdi-radio-tower::before {
+ content: "\F43B";
+}
+.mdi-radioactive::before {
+ content: "\F43C";
+}
+.mdi-radioactive-off::before {
+ content: "\FEDE";
+}
+.mdi-radiobox-blank::before {
+ content: "\F43D";
+}
+.mdi-radiobox-marked::before {
+ content: "\F43E";
+}
+.mdi-radius::before {
+ content: "\FC9C";
+}
+.mdi-radius-outline::before {
+ content: "\FC9D";
+}
+.mdi-railroad-light::before {
+ content: "\FF3B";
+}
+.mdi-raspberry-pi::before {
+ content: "\F43F";
+}
+.mdi-ray-end::before {
+ content: "\F440";
+}
+.mdi-ray-end-arrow::before {
+ content: "\F441";
+}
+.mdi-ray-start::before {
+ content: "\F442";
+}
+.mdi-ray-start-arrow::before {
+ content: "\F443";
+}
+.mdi-ray-start-end::before {
+ content: "\F444";
+}
+.mdi-ray-vertex::before {
+ content: "\F445";
+}
+.mdi-react::before {
+ content: "\F707";
+}
+.mdi-read::before {
+ content: "\F447";
+}
+.mdi-receipt::before {
+ content: "\F449";
+}
+.mdi-record::before {
+ content: "\F44A";
+}
+.mdi-record-circle::before {
+ content: "\FEDF";
+}
+.mdi-record-circle-outline::before {
+ content: "\FEE0";
+}
+.mdi-record-player::before {
+ content: "\F999";
+}
+.mdi-record-rec::before {
+ content: "\F44B";
+}
+.mdi-rectangle::before {
+ content: "\FE41";
+}
+.mdi-rectangle-outline::before {
+ content: "\FE42";
+}
+.mdi-recycle::before {
+ content: "\F44C";
+}
+.mdi-reddit::before {
+ content: "\F44D";
+}
+.mdi-redhat::before {
+ content: "\F0146";
+}
+.mdi-redo::before {
+ content: "\F44E";
+}
+.mdi-redo-variant::before {
+ content: "\F44F";
+}
+.mdi-reflect-horizontal::before {
+ content: "\FA0D";
+}
+.mdi-reflect-vertical::before {
+ content: "\FA0E";
+}
+.mdi-refresh::before {
+ content: "\F450";
+}
+.mdi-refresh-circle::before {
+ content: "\F03A2";
+}
+.mdi-regex::before {
+ content: "\F451";
+}
+.mdi-registered-trademark::before {
+ content: "\FA66";
+}
+.mdi-relative-scale::before {
+ content: "\F452";
+}
+.mdi-reload::before {
+ content: "\F453";
+}
+.mdi-reload-alert::before {
+ content: "\F0136";
+}
+.mdi-reminder::before {
+ content: "\F88B";
+}
+.mdi-remote::before {
+ content: "\F454";
+}
+.mdi-remote-desktop::before {
+ content: "\F8B8";
+}
+.mdi-remote-off::before {
+ content: "\FEE1";
+}
+.mdi-remote-tv::before {
+ content: "\FEE2";
+}
+.mdi-remote-tv-off::before {
+ content: "\FEE3";
+}
+.mdi-rename-box::before {
+ content: "\F455";
+}
+.mdi-reorder-horizontal::before {
+ content: "\F687";
+}
+.mdi-reorder-vertical::before {
+ content: "\F688";
+}
+.mdi-repeat::before {
+ content: "\F456";
+}
+.mdi-repeat-off::before {
+ content: "\F457";
+}
+.mdi-repeat-once::before {
+ content: "\F458";
+}
+.mdi-replay::before {
+ content: "\F459";
+}
+.mdi-reply::before {
+ content: "\F45A";
+}
+.mdi-reply-all::before {
+ content: "\F45B";
+}
+.mdi-reply-all-outline::before {
+ content: "\FF3C";
+}
+.mdi-reply-circle::before {
+ content: "\F01D9";
+}
+.mdi-reply-outline::before {
+ content: "\FF3D";
+}
+.mdi-reproduction::before {
+ content: "\F45C";
+}
+.mdi-resistor::before {
+ content: "\FB1F";
+}
+.mdi-resistor-nodes::before {
+ content: "\FB20";
+}
+.mdi-resize::before {
+ content: "\FA67";
+}
+.mdi-resize-bottom-right::before {
+ content: "\F45D";
+}
+.mdi-responsive::before {
+ content: "\F45E";
+}
+.mdi-restart::before {
+ content: "\F708";
+}
+.mdi-restart-alert::before {
+ content: "\F0137";
+}
+.mdi-restart-off::before {
+ content: "\FD71";
+}
+.mdi-restore::before {
+ content: "\F99A";
+}
+.mdi-restore-alert::before {
+ content: "\F0138";
+}
+.mdi-rewind::before {
+ content: "\F45F";
+}
+.mdi-rewind-10::before {
+ content: "\FD06";
+}
+.mdi-rewind-30::before {
+ content: "\FD72";
+}
+.mdi-rewind-5::before {
+ content: "\F0224";
+}
+.mdi-rewind-outline::before {
+ content: "\F709";
+}
+.mdi-rhombus::before {
+ content: "\F70A";
+}
+.mdi-rhombus-medium::before {
+ content: "\FA0F";
+}
+.mdi-rhombus-outline::before {
+ content: "\F70B";
+}
+.mdi-rhombus-split::before {
+ content: "\FA10";
+}
+.mdi-ribbon::before {
+ content: "\F460";
+}
+.mdi-rice::before {
+ content: "\F7E9";
+}
+.mdi-ring::before {
+ content: "\F7EA";
+}
+.mdi-rivet::before {
+ content: "\FE43";
+}
+.mdi-road::before {
+ content: "\F461";
+}
+.mdi-road-variant::before {
+ content: "\F462";
+}
+.mdi-robber::before {
+ content: "\F007A";
+}
+.mdi-robot::before {
+ content: "\F6A8";
+}
+.mdi-robot-industrial::before {
+ content: "\FB21";
+}
+.mdi-robot-mower::before {
+ content: "\F0222";
+}
+.mdi-robot-mower-outline::before {
+ content: "\F021E";
+}
+.mdi-robot-vacuum::before {
+ content: "\F70C";
+}
+.mdi-robot-vacuum-variant::before {
+ content: "\F907";
+}
+.mdi-rocket::before {
+ content: "\F463";
+}
+.mdi-rodent::before {
+ content: "\F0352";
+}
+.mdi-roller-skate::before {
+ content: "\FD07";
+}
+.mdi-rollerblade::before {
+ content: "\FD08";
+}
+.mdi-rollupjs::before {
+ content: "\FB9C";
+}
+.mdi-roman-numeral-1::before {
+ content: "\F00B3";
+}
+.mdi-roman-numeral-10::before {
+ content: "\F00BC";
+}
+.mdi-roman-numeral-2::before {
+ content: "\F00B4";
+}
+.mdi-roman-numeral-3::before {
+ content: "\F00B5";
+}
+.mdi-roman-numeral-4::before {
+ content: "\F00B6";
+}
+.mdi-roman-numeral-5::before {
+ content: "\F00B7";
+}
+.mdi-roman-numeral-6::before {
+ content: "\F00B8";
+}
+.mdi-roman-numeral-7::before {
+ content: "\F00B9";
+}
+.mdi-roman-numeral-8::before {
+ content: "\F00BA";
+}
+.mdi-roman-numeral-9::before {
+ content: "\F00BB";
+}
+.mdi-room-service::before {
+ content: "\F88C";
+}
+.mdi-room-service-outline::before {
+ content: "\FD73";
+}
+.mdi-rotate-3d::before {
+ content: "\FEE4";
+}
+.mdi-rotate-3d-variant::before {
+ content: "\F464";
+}
+.mdi-rotate-left::before {
+ content: "\F465";
+}
+.mdi-rotate-left-variant::before {
+ content: "\F466";
+}
+.mdi-rotate-orbit::before {
+ content: "\FD74";
+}
+.mdi-rotate-right::before {
+ content: "\F467";
+}
+.mdi-rotate-right-variant::before {
+ content: "\F468";
+}
+.mdi-rounded-corner::before {
+ content: "\F607";
+}
+.mdi-router::before {
+ content: "\F020D";
+}
+.mdi-router-wireless::before {
+ content: "\F469";
+}
+.mdi-router-wireless-settings::before {
+ content: "\FA68";
+}
+.mdi-routes::before {
+ content: "\F46A";
+}
+.mdi-routes-clock::before {
+ content: "\F007B";
+}
+.mdi-rowing::before {
+ content: "\F608";
+}
+.mdi-rss::before {
+ content: "\F46B";
+}
+.mdi-rss-box::before {
+ content: "\F46C";
+}
+.mdi-rss-off::before {
+ content: "\FF3E";
+}
+.mdi-ruby::before {
+ content: "\FD09";
+}
+.mdi-rugby::before {
+ content: "\FD75";
+}
+.mdi-ruler::before {
+ content: "\F46D";
+}
+.mdi-ruler-square::before {
+ content: "\FC9E";
+}
+.mdi-ruler-square-compass::before {
+ content: "\FEDB";
+}
+.mdi-run::before {
+ content: "\F70D";
+}
+.mdi-run-fast::before {
+ content: "\F46E";
+}
+.mdi-rv-truck::before {
+ content: "\F01FF";
+}
+.mdi-sack::before {
+ content: "\FD0A";
+}
+.mdi-sack-percent::before {
+ content: "\FD0B";
+}
+.mdi-safe::before {
+ content: "\FA69";
+}
+.mdi-safe-square::before {
+ content: "\F02A7";
+}
+.mdi-safe-square-outline::before {
+ content: "\F02A8";
+}
+.mdi-safety-goggles::before {
+ content: "\FD0C";
+}
+.mdi-sailing::before {
+ content: "\FEE5";
+}
+.mdi-sale::before {
+ content: "\F46F";
+}
+.mdi-salesforce::before {
+ content: "\F88D";
+}
+.mdi-sass::before {
+ content: "\F7EB";
+}
+.mdi-satellite::before {
+ content: "\F470";
+}
+.mdi-satellite-uplink::before {
+ content: "\F908";
+}
+.mdi-satellite-variant::before {
+ content: "\F471";
+}
+.mdi-sausage::before {
+ content: "\F8B9";
+}
+.mdi-saw-blade::before {
+ content: "\FE44";
+}
+.mdi-saxophone::before {
+ content: "\F609";
+}
+.mdi-scale::before {
+ content: "\F472";
+}
+.mdi-scale-balance::before {
+ content: "\F5D1";
+}
+.mdi-scale-bathroom::before {
+ content: "\F473";
+}
+.mdi-scale-off::before {
+ content: "\F007C";
+}
+.mdi-scanner::before {
+ content: "\F6AA";
+}
+.mdi-scanner-off::before {
+ content: "\F909";
+}
+.mdi-scatter-plot::before {
+ content: "\FEE6";
+}
+.mdi-scatter-plot-outline::before {
+ content: "\FEE7";
+}
+.mdi-school::before {
+ content: "\F474";
+}
+.mdi-school-outline::before {
+ content: "\F01AB";
+}
+.mdi-scissors-cutting::before {
+ content: "\FA6A";
+}
+.mdi-scooter::before {
+ content: "\F0214";
+}
+.mdi-scoreboard::before {
+ content: "\F02A9";
+}
+.mdi-scoreboard-outline::before {
+ content: "\F02AA";
+}
+.mdi-screen-rotation::before {
+ content: "\F475";
+}
+.mdi-screen-rotation-lock::before {
+ content: "\F476";
+}
+.mdi-screw-flat-top::before {
+ content: "\FDCF";
+}
+.mdi-screw-lag::before {
+ content: "\FE54";
+}
+.mdi-screw-machine-flat-top::before {
+ content: "\FE55";
+}
+.mdi-screw-machine-round-top::before {
+ content: "\FE56";
+}
+.mdi-screw-round-top::before {
+ content: "\FE57";
+}
+.mdi-screwdriver::before {
+ content: "\F477";
+}
+.mdi-script::before {
+ content: "\FB9D";
+}
+.mdi-script-outline::before {
+ content: "\F478";
+}
+.mdi-script-text::before {
+ content: "\FB9E";
+}
+.mdi-script-text-outline::before {
+ content: "\FB9F";
+}
+.mdi-sd::before {
+ content: "\F479";
+}
+.mdi-seal::before {
+ content: "\F47A";
+}
+.mdi-seal-variant::before {
+ content: "\FFFA";
+}
+.mdi-search-web::before {
+ content: "\F70E";
+}
+.mdi-seat::before {
+ content: "\FC9F";
+}
+.mdi-seat-flat::before {
+ content: "\F47B";
+}
+.mdi-seat-flat-angled::before {
+ content: "\F47C";
+}
+.mdi-seat-individual-suite::before {
+ content: "\F47D";
+}
+.mdi-seat-legroom-extra::before {
+ content: "\F47E";
+}
+.mdi-seat-legroom-normal::before {
+ content: "\F47F";
+}
+.mdi-seat-legroom-reduced::before {
+ content: "\F480";
+}
+.mdi-seat-outline::before {
+ content: "\FCA0";
+}
+.mdi-seat-passenger::before {
+ content: "\F0274";
+}
+.mdi-seat-recline-extra::before {
+ content: "\F481";
+}
+.mdi-seat-recline-normal::before {
+ content: "\F482";
+}
+.mdi-seatbelt::before {
+ content: "\FCA1";
+}
+.mdi-security::before {
+ content: "\F483";
+}
+.mdi-security-network::before {
+ content: "\F484";
+}
+.mdi-seed::before {
+ content: "\FE45";
+}
+.mdi-seed-outline::before {
+ content: "\FE46";
+}
+.mdi-segment::before {
+ content: "\FEE8";
+}
+.mdi-select::before {
+ content: "\F485";
+}
+.mdi-select-all::before {
+ content: "\F486";
+}
+.mdi-select-color::before {
+ content: "\FD0D";
+}
+.mdi-select-compare::before {
+ content: "\FAD8";
+}
+.mdi-select-drag::before {
+ content: "\FA6B";
+}
+.mdi-select-group::before {
+ content: "\FF9F";
+}
+.mdi-select-inverse::before {
+ content: "\F487";
+}
+.mdi-select-marker::before {
+ content: "\F02AB";
+}
+.mdi-select-multiple::before {
+ content: "\F02AC";
+}
+.mdi-select-multiple-marker::before {
+ content: "\F02AD";
+}
+.mdi-select-off::before {
+ content: "\F488";
+}
+.mdi-select-place::before {
+ content: "\FFFB";
+}
+.mdi-select-search::before {
+ content: "\F022F";
+}
+.mdi-selection::before {
+ content: "\F489";
+}
+.mdi-selection-drag::before {
+ content: "\FA6C";
+}
+.mdi-selection-ellipse::before {
+ content: "\FD0E";
+}
+.mdi-selection-ellipse-arrow-inside::before {
+ content: "\FF3F";
+}
+.mdi-selection-marker::before {
+ content: "\F02AE";
+}
+.mdi-selection-multiple-marker::before {
+ content: "\F02AF";
+}
+.mdi-selection-mutliple::before {
+ content: "\F02B0";
+}
+.mdi-selection-off::before {
+ content: "\F776";
+}
+.mdi-selection-search::before {
+ content: "\F0230";
+}
+.mdi-semantic-web::before {
+ content: "\F0341";
+}
+.mdi-send::before {
+ content: "\F48A";
+}
+.mdi-send-check::before {
+ content: "\F018C";
+}
+.mdi-send-check-outline::before {
+ content: "\F018D";
+}
+.mdi-send-circle::before {
+ content: "\FE58";
+}
+.mdi-send-circle-outline::before {
+ content: "\FE59";
+}
+.mdi-send-clock::before {
+ content: "\F018E";
+}
+.mdi-send-clock-outline::before {
+ content: "\F018F";
+}
+.mdi-send-lock::before {
+ content: "\F7EC";
+}
+.mdi-send-lock-outline::before {
+ content: "\F0191";
+}
+.mdi-send-outline::before {
+ content: "\F0190";
+}
+.mdi-serial-port::before {
+ content: "\F65C";
+}
+.mdi-server::before {
+ content: "\F48B";
+}
+.mdi-server-minus::before {
+ content: "\F48C";
+}
+.mdi-server-network::before {
+ content: "\F48D";
+}
+.mdi-server-network-off::before {
+ content: "\F48E";
+}
+.mdi-server-off::before {
+ content: "\F48F";
+}
+.mdi-server-plus::before {
+ content: "\F490";
+}
+.mdi-server-remove::before {
+ content: "\F491";
+}
+.mdi-server-security::before {
+ content: "\F492";
+}
+.mdi-set-all::before {
+ content: "\F777";
+}
+.mdi-set-center::before {
+ content: "\F778";
+}
+.mdi-set-center-right::before {
+ content: "\F779";
+}
+.mdi-set-left::before {
+ content: "\F77A";
+}
+.mdi-set-left-center::before {
+ content: "\F77B";
+}
+.mdi-set-left-right::before {
+ content: "\F77C";
+}
+.mdi-set-none::before {
+ content: "\F77D";
+}
+.mdi-set-right::before {
+ content: "\F77E";
+}
+.mdi-set-top-box::before {
+ content: "\F99E";
+}
+.mdi-settings::before {
+ content: "\F493";
+}
+.mdi-settings-box::before {
+ content: "\F494";
+}
+.mdi-settings-helper::before {
+ content: "\FA6D";
+}
+.mdi-settings-outline::before {
+ content: "\F8BA";
+}
+.mdi-settings-transfer::before {
+ content: "\F007D";
+}
+.mdi-settings-transfer-outline::before {
+ content: "\F007E";
+}
+.mdi-shaker::before {
+ content: "\F0139";
+}
+.mdi-shaker-outline::before {
+ content: "\F013A";
+}
+.mdi-shape::before {
+ content: "\F830";
+}
+.mdi-shape-circle-plus::before {
+ content: "\F65D";
+}
+.mdi-shape-outline::before {
+ content: "\F831";
+}
+.mdi-shape-oval-plus::before {
+ content: "\F0225";
+}
+.mdi-shape-plus::before {
+ content: "\F495";
+}
+.mdi-shape-polygon-plus::before {
+ content: "\F65E";
+}
+.mdi-shape-rectangle-plus::before {
+ content: "\F65F";
+}
+.mdi-shape-square-plus::before {
+ content: "\F660";
+}
+.mdi-share::before {
+ content: "\F496";
+}
+.mdi-share-all::before {
+ content: "\F021F";
+}
+.mdi-share-all-outline::before {
+ content: "\F0220";
+}
+.mdi-share-circle::before {
+ content: "\F01D8";
+}
+.mdi-share-off::before {
+ content: "\FF40";
+}
+.mdi-share-off-outline::before {
+ content: "\FF41";
+}
+.mdi-share-outline::before {
+ content: "\F931";
+}
+.mdi-share-variant::before {
+ content: "\F497";
+}
+.mdi-sheep::before {
+ content: "\FCA2";
+}
+.mdi-shield::before {
+ content: "\F498";
+}
+.mdi-shield-account::before {
+ content: "\F88E";
+}
+.mdi-shield-account-outline::before {
+ content: "\FA11";
+}
+.mdi-shield-airplane::before {
+ content: "\F6BA";
+}
+.mdi-shield-airplane-outline::before {
+ content: "\FCA3";
+}
+.mdi-shield-alert::before {
+ content: "\FEE9";
+}
+.mdi-shield-alert-outline::before {
+ content: "\FEEA";
+}
+.mdi-shield-car::before {
+ content: "\FFA0";
+}
+.mdi-shield-check::before {
+ content: "\F565";
+}
+.mdi-shield-check-outline::before {
+ content: "\FCA4";
+}
+.mdi-shield-cross::before {
+ content: "\FCA5";
+}
+.mdi-shield-cross-outline::before {
+ content: "\FCA6";
+}
+.mdi-shield-edit::before {
+ content: "\F01CB";
+}
+.mdi-shield-edit-outline::before {
+ content: "\F01CC";
+}
+.mdi-shield-half::before {
+ content: "\F038B";
+}
+.mdi-shield-half-full::before {
+ content: "\F77F";
+}
+.mdi-shield-home::before {
+ content: "\F689";
+}
+.mdi-shield-home-outline::before {
+ content: "\FCA7";
+}
+.mdi-shield-key::before {
+ content: "\FBA0";
+}
+.mdi-shield-key-outline::before {
+ content: "\FBA1";
+}
+.mdi-shield-link-variant::before {
+ content: "\FD0F";
+}
+.mdi-shield-link-variant-outline::before {
+ content: "\FD10";
+}
+.mdi-shield-lock::before {
+ content: "\F99C";
+}
+.mdi-shield-lock-outline::before {
+ content: "\FCA8";
+}
+.mdi-shield-off::before {
+ content: "\F99D";
+}
+.mdi-shield-off-outline::before {
+ content: "\F99B";
+}
+.mdi-shield-outline::before {
+ content: "\F499";
+}
+.mdi-shield-plus::before {
+ content: "\FAD9";
+}
+.mdi-shield-plus-outline::before {
+ content: "\FADA";
+}
+.mdi-shield-refresh::before {
+ content: "\F01CD";
+}
+.mdi-shield-refresh-outline::before {
+ content: "\F01CE";
+}
+.mdi-shield-remove::before {
+ content: "\FADB";
+}
+.mdi-shield-remove-outline::before {
+ content: "\FADC";
+}
+.mdi-shield-search::before {
+ content: "\FD76";
+}
+.mdi-shield-star::before {
+ content: "\F0166";
+}
+.mdi-shield-star-outline::before {
+ content: "\F0167";
+}
+.mdi-shield-sun::before {
+ content: "\F007F";
+}
+.mdi-shield-sun-outline::before {
+ content: "\F0080";
+}
+.mdi-ship-wheel::before {
+ content: "\F832";
+}
+.mdi-shoe-formal::before {
+ content: "\FB22";
+}
+.mdi-shoe-heel::before {
+ content: "\FB23";
+}
+.mdi-shoe-print::before {
+ content: "\FE5A";
+}
+.mdi-shopify::before {
+ content: "\FADD";
+}
+.mdi-shopping::before {
+ content: "\F49A";
+}
+.mdi-shopping-music::before {
+ content: "\F49B";
+}
+.mdi-shopping-outline::before {
+ content: "\F0200";
+}
+.mdi-shopping-search::before {
+ content: "\FFA1";
+}
+.mdi-shovel::before {
+ content: "\F70F";
+}
+.mdi-shovel-off::before {
+ content: "\F710";
+}
+.mdi-shower::before {
+ content: "\F99F";
+}
+.mdi-shower-head::before {
+ content: "\F9A0";
+}
+.mdi-shredder::before {
+ content: "\F49C";
+}
+.mdi-shuffle::before {
+ content: "\F49D";
+}
+.mdi-shuffle-disabled::before {
+ content: "\F49E";
+}
+.mdi-shuffle-variant::before {
+ content: "\F49F";
+}
+.mdi-shuriken::before {
+ content: "\F03AA";
+}
+.mdi-sigma::before {
+ content: "\F4A0";
+}
+.mdi-sigma-lower::before {
+ content: "\F62B";
+}
+.mdi-sign-caution::before {
+ content: "\F4A1";
+}
+.mdi-sign-direction::before {
+ content: "\F780";
+}
+.mdi-sign-direction-minus::before {
+ content: "\F0022";
+}
+.mdi-sign-direction-plus::before {
+ content: "\FFFD";
+}
+.mdi-sign-direction-remove::before {
+ content: "\FFFE";
+}
+.mdi-sign-real-estate::before {
+ content: "\F0143";
+}
+.mdi-sign-text::before {
+ content: "\F781";
+}
+.mdi-signal::before {
+ content: "\F4A2";
+}
+.mdi-signal-2g::before {
+ content: "\F711";
+}
+.mdi-signal-3g::before {
+ content: "\F712";
+}
+.mdi-signal-4g::before {
+ content: "\F713";
+}
+.mdi-signal-5g::before {
+ content: "\FA6E";
+}
+.mdi-signal-cellular-1::before {
+ content: "\F8BB";
+}
+.mdi-signal-cellular-2::before {
+ content: "\F8BC";
+}
+.mdi-signal-cellular-3::before {
+ content: "\F8BD";
+}
+.mdi-signal-cellular-outline::before {
+ content: "\F8BE";
+}
+.mdi-signal-distance-variant::before {
+ content: "\FE47";
+}
+.mdi-signal-hspa::before {
+ content: "\F714";
+}
+.mdi-signal-hspa-plus::before {
+ content: "\F715";
+}
+.mdi-signal-off::before {
+ content: "\F782";
+}
+.mdi-signal-variant::before {
+ content: "\F60A";
+}
+.mdi-signature::before {
+ content: "\FE5B";
+}
+.mdi-signature-freehand::before {
+ content: "\FE5C";
+}
+.mdi-signature-image::before {
+ content: "\FE5D";
+}
+.mdi-signature-text::before {
+ content: "\FE5E";
+}
+.mdi-silo::before {
+ content: "\FB24";
+}
+.mdi-silverware::before {
+ content: "\F4A3";
+}
+.mdi-silverware-clean::before {
+ content: "\FFFF";
+}
+.mdi-silverware-fork::before {
+ content: "\F4A4";
+}
+.mdi-silverware-fork-knife::before {
+ content: "\FA6F";
+}
+.mdi-silverware-spoon::before {
+ content: "\F4A5";
+}
+.mdi-silverware-variant::before {
+ content: "\F4A6";
+}
+.mdi-sim::before {
+ content: "\F4A7";
+}
+.mdi-sim-alert::before {
+ content: "\F4A8";
+}
+.mdi-sim-off::before {
+ content: "\F4A9";
+}
+.mdi-simple-icons::before {
+ content: "\F0348";
+}
+.mdi-sina-weibo::before {
+ content: "\FADE";
+}
+.mdi-sitemap::before {
+ content: "\F4AA";
+}
+.mdi-skate::before {
+ content: "\FD11";
+}
+.mdi-skew-less::before {
+ content: "\FD12";
+}
+.mdi-skew-more::before {
+ content: "\FD13";
+}
+.mdi-ski::before {
+ content: "\F032F";
+}
+.mdi-ski-cross-country::before {
+ content: "\F0330";
+}
+.mdi-ski-water::before {
+ content: "\F0331";
+}
+.mdi-skip-backward::before {
+ content: "\F4AB";
+}
+.mdi-skip-backward-outline::before {
+ content: "\FF42";
+}
+.mdi-skip-forward::before {
+ content: "\F4AC";
+}
+.mdi-skip-forward-outline::before {
+ content: "\FF43";
+}
+.mdi-skip-next::before {
+ content: "\F4AD";
+}
+.mdi-skip-next-circle::before {
+ content: "\F661";
+}
+.mdi-skip-next-circle-outline::before {
+ content: "\F662";
+}
+.mdi-skip-next-outline::before {
+ content: "\FF44";
+}
+.mdi-skip-previous::before {
+ content: "\F4AE";
+}
+.mdi-skip-previous-circle::before {
+ content: "\F663";
+}
+.mdi-skip-previous-circle-outline::before {
+ content: "\F664";
+}
+.mdi-skip-previous-outline::before {
+ content: "\FF45";
+}
+.mdi-skull::before {
+ content: "\F68B";
+}
+.mdi-skull-crossbones::before {
+ content: "\FBA2";
+}
+.mdi-skull-crossbones-outline::before {
+ content: "\FBA3";
+}
+.mdi-skull-outline::before {
+ content: "\FBA4";
+}
+.mdi-skype::before {
+ content: "\F4AF";
+}
+.mdi-skype-business::before {
+ content: "\F4B0";
+}
+.mdi-slack::before {
+ content: "\F4B1";
+}
+.mdi-slackware::before {
+ content: "\F90A";
+}
+.mdi-slash-forward::before {
+ content: "\F0000";
+}
+.mdi-slash-forward-box::before {
+ content: "\F0001";
+}
+.mdi-sleep::before {
+ content: "\F4B2";
+}
+.mdi-sleep-off::before {
+ content: "\F4B3";
+}
+.mdi-slope-downhill::before {
+ content: "\FE5F";
+}
+.mdi-slope-uphill::before {
+ content: "\FE60";
+}
+.mdi-slot-machine::before {
+ content: "\F013F";
+}
+.mdi-slot-machine-outline::before {
+ content: "\F0140";
+}
+.mdi-smart-card::before {
+ content: "\F00E8";
+}
+.mdi-smart-card-outline::before {
+ content: "\F00E9";
+}
+.mdi-smart-card-reader::before {
+ content: "\F00EA";
+}
+.mdi-smart-card-reader-outline::before {
+ content: "\F00EB";
+}
+.mdi-smog::before {
+ content: "\FA70";
+}
+.mdi-smoke-detector::before {
+ content: "\F392";
+}
+.mdi-smoking::before {
+ content: "\F4B4";
+}
+.mdi-smoking-off::before {
+ content: "\F4B5";
+}
+.mdi-snapchat::before {
+ content: "\F4B6";
+}
+.mdi-snowboard::before {
+ content: "\F0332";
+}
+.mdi-snowflake::before {
+ content: "\F716";
+}
+.mdi-snowflake-alert::before {
+ content: "\FF46";
+}
+.mdi-snowflake-melt::before {
+ content: "\F02F6";
+}
+.mdi-snowflake-variant::before {
+ content: "\FF47";
+}
+.mdi-snowman::before {
+ content: "\F4B7";
+}
+.mdi-soccer::before {
+ content: "\F4B8";
+}
+.mdi-soccer-field::before {
+ content: "\F833";
+}
+.mdi-sofa::before {
+ content: "\F4B9";
+}
+.mdi-solar-panel::before {
+ content: "\FD77";
+}
+.mdi-solar-panel-large::before {
+ content: "\FD78";
+}
+.mdi-solar-power::before {
+ content: "\FA71";
+}
+.mdi-soldering-iron::before {
+ content: "\F00BD";
+}
+.mdi-solid::before {
+ content: "\F68C";
+}
+.mdi-sort::before {
+ content: "\F4BA";
+}
+.mdi-sort-alphabetical::before {
+ content: "\F4BB";
+}
+.mdi-sort-alphabetical-ascending::before {
+ content: "\F0173";
+}
+.mdi-sort-alphabetical-descending::before {
+ content: "\F0174";
+}
+.mdi-sort-ascending::before {
+ content: "\F4BC";
+}
+.mdi-sort-descending::before {
+ content: "\F4BD";
+}
+.mdi-sort-numeric::before {
+ content: "\F4BE";
+}
+.mdi-sort-variant::before {
+ content: "\F4BF";
+}
+.mdi-sort-variant-lock::before {
+ content: "\FCA9";
+}
+.mdi-sort-variant-lock-open::before {
+ content: "\FCAA";
+}
+.mdi-sort-variant-remove::before {
+ content: "\F0172";
+}
+.mdi-soundcloud::before {
+ content: "\F4C0";
+}
+.mdi-source-branch::before {
+ content: "\F62C";
+}
+.mdi-source-commit::before {
+ content: "\F717";
+}
+.mdi-source-commit-end::before {
+ content: "\F718";
+}
+.mdi-source-commit-end-local::before {
+ content: "\F719";
+}
+.mdi-source-commit-local::before {
+ content: "\F71A";
+}
+.mdi-source-commit-next-local::before {
+ content: "\F71B";
+}
+.mdi-source-commit-start::before {
+ content: "\F71C";
+}
+.mdi-source-commit-start-next-local::before {
+ content: "\F71D";
+}
+.mdi-source-fork::before {
+ content: "\F4C1";
+}
+.mdi-source-merge::before {
+ content: "\F62D";
+}
+.mdi-source-pull::before {
+ content: "\F4C2";
+}
+.mdi-source-repository::before {
+ content: "\FCAB";
+}
+.mdi-source-repository-multiple::before {
+ content: "\FCAC";
+}
+.mdi-soy-sauce::before {
+ content: "\F7ED";
+}
+.mdi-spa::before {
+ content: "\FCAD";
+}
+.mdi-spa-outline::before {
+ content: "\FCAE";
+}
+.mdi-space-invaders::before {
+ content: "\FBA5";
+}
+.mdi-space-station::before {
+ content: "\F03AE";
+}
+.mdi-spade::before {
+ content: "\FE48";
+}
+.mdi-speaker::before {
+ content: "\F4C3";
+}
+.mdi-speaker-bluetooth::before {
+ content: "\F9A1";
+}
+.mdi-speaker-multiple::before {
+ content: "\FD14";
+}
+.mdi-speaker-off::before {
+ content: "\F4C4";
+}
+.mdi-speaker-wireless::before {
+ content: "\F71E";
+}
+.mdi-speedometer::before {
+ content: "\F4C5";
+}
+.mdi-speedometer-medium::before {
+ content: "\FFA2";
+}
+.mdi-speedometer-slow::before {
+ content: "\FFA3";
+}
+.mdi-spellcheck::before {
+ content: "\F4C6";
+}
+.mdi-spider::before {
+ content: "\F0215";
+}
+.mdi-spider-thread::before {
+ content: "\F0216";
+}
+.mdi-spider-web::before {
+ content: "\FBA6";
+}
+.mdi-spotify::before {
+ content: "\F4C7";
+}
+.mdi-spotlight::before {
+ content: "\F4C8";
+}
+.mdi-spotlight-beam::before {
+ content: "\F4C9";
+}
+.mdi-spray::before {
+ content: "\F665";
+}
+.mdi-spray-bottle::before {
+ content: "\FADF";
+}
+.mdi-sprinkler::before {
+ content: "\F0081";
+}
+.mdi-sprinkler-variant::before {
+ content: "\F0082";
+}
+.mdi-sprout::before {
+ content: "\FE49";
+}
+.mdi-sprout-outline::before {
+ content: "\FE4A";
+}
+.mdi-square::before {
+ content: "\F763";
+}
+.mdi-square-edit-outline::before {
+ content: "\F90B";
+}
+.mdi-square-inc::before {
+ content: "\F4CA";
+}
+.mdi-square-inc-cash::before {
+ content: "\F4CB";
+}
+.mdi-square-medium::before {
+ content: "\FA12";
+}
+.mdi-square-medium-outline::before {
+ content: "\FA13";
+}
+.mdi-square-off::before {
+ content: "\F0319";
+}
+.mdi-square-off-outline::before {
+ content: "\F031A";
+}
+.mdi-square-outline::before {
+ content: "\F762";
+}
+.mdi-square-root::before {
+ content: "\F783";
+}
+.mdi-square-root-box::before {
+ content: "\F9A2";
+}
+.mdi-square-small::before {
+ content: "\FA14";
+}
+.mdi-squeegee::before {
+ content: "\FAE0";
+}
+.mdi-ssh::before {
+ content: "\F8BF";
+}
+.mdi-stack-exchange::before {
+ content: "\F60B";
+}
+.mdi-stack-overflow::before {
+ content: "\F4CC";
+}
+.mdi-stackpath::before {
+ content: "\F359";
+}
+.mdi-stadium::before {
+ content: "\F001A";
+}
+.mdi-stadium-variant::before {
+ content: "\F71F";
+}
+.mdi-stairs::before {
+ content: "\F4CD";
+}
+.mdi-stairs-down::before {
+ content: "\F02E9";
+}
+.mdi-stairs-up::before {
+ content: "\F02E8";
+}
+.mdi-stamper::before {
+ content: "\FD15";
+}
+.mdi-standard-definition::before {
+ content: "\F7EE";
+}
+.mdi-star::before {
+ content: "\F4CE";
+}
+.mdi-star-box::before {
+ content: "\FA72";
+}
+.mdi-star-box-multiple::before {
+ content: "\F02B1";
+}
+.mdi-star-box-multiple-outline::before {
+ content: "\F02B2";
+}
+.mdi-star-box-outline::before {
+ content: "\FA73";
+}
+.mdi-star-circle::before {
+ content: "\F4CF";
+}
+.mdi-star-circle-outline::before {
+ content: "\F9A3";
+}
+.mdi-star-face::before {
+ content: "\F9A4";
+}
+.mdi-star-four-points::before {
+ content: "\FAE1";
+}
+.mdi-star-four-points-outline::before {
+ content: "\FAE2";
+}
+.mdi-star-half::before {
+ content: "\F4D0";
+}
+.mdi-star-off::before {
+ content: "\F4D1";
+}
+.mdi-star-outline::before {
+ content: "\F4D2";
+}
+.mdi-star-three-points::before {
+ content: "\FAE3";
+}
+.mdi-star-three-points-outline::before {
+ content: "\FAE4";
+}
+.mdi-state-machine::before {
+ content: "\F021A";
+}
+.mdi-steam::before {
+ content: "\F4D3";
+}
+.mdi-steam-box::before {
+ content: "\F90C";
+}
+.mdi-steering::before {
+ content: "\F4D4";
+}
+.mdi-steering-off::before {
+ content: "\F90D";
+}
+.mdi-step-backward::before {
+ content: "\F4D5";
+}
+.mdi-step-backward-2::before {
+ content: "\F4D6";
+}
+.mdi-step-forward::before {
+ content: "\F4D7";
+}
+.mdi-step-forward-2::before {
+ content: "\F4D8";
+}
+.mdi-stethoscope::before {
+ content: "\F4D9";
+}
+.mdi-sticker::before {
+ content: "\F038F";
+}
+.mdi-sticker-alert::before {
+ content: "\F0390";
+}
+.mdi-sticker-alert-outline::before {
+ content: "\F0391";
+}
+.mdi-sticker-check::before {
+ content: "\F0392";
+}
+.mdi-sticker-check-outline::before {
+ content: "\F0393";
+}
+.mdi-sticker-circle-outline::before {
+ content: "\F5D0";
+}
+.mdi-sticker-emoji::before {
+ content: "\F784";
+}
+.mdi-sticker-minus::before {
+ content: "\F0394";
+}
+.mdi-sticker-minus-outline::before {
+ content: "\F0395";
+}
+.mdi-sticker-outline::before {
+ content: "\F0396";
+}
+.mdi-sticker-plus::before {
+ content: "\F0397";
+}
+.mdi-sticker-plus-outline::before {
+ content: "\F0398";
+}
+.mdi-sticker-remove::before {
+ content: "\F0399";
+}
+.mdi-sticker-remove-outline::before {
+ content: "\F039A";
+}
+.mdi-stocking::before {
+ content: "\F4DA";
+}
+.mdi-stomach::before {
+ content: "\F00BE";
+}
+.mdi-stop::before {
+ content: "\F4DB";
+}
+.mdi-stop-circle::before {
+ content: "\F666";
+}
+.mdi-stop-circle-outline::before {
+ content: "\F667";
+}
+.mdi-store::before {
+ content: "\F4DC";
+}
+.mdi-store-24-hour::before {
+ content: "\F4DD";
+}
+.mdi-store-outline::before {
+ content: "\F038C";
+}
+.mdi-storefront::before {
+ content: "\F00EC";
+}
+.mdi-stove::before {
+ content: "\F4DE";
+}
+.mdi-strategy::before {
+ content: "\F0201";
+}
+.mdi-strava::before {
+ content: "\FB25";
+}
+.mdi-stretch-to-page::before {
+ content: "\FF48";
+}
+.mdi-stretch-to-page-outline::before {
+ content: "\FF49";
+}
+.mdi-string-lights::before {
+ content: "\F02E5";
+}
+.mdi-string-lights-off::before {
+ content: "\F02E6";
+}
+.mdi-subdirectory-arrow-left::before {
+ content: "\F60C";
+}
+.mdi-subdirectory-arrow-right::before {
+ content: "\F60D";
+}
+.mdi-subtitles::before {
+ content: "\FA15";
+}
+.mdi-subtitles-outline::before {
+ content: "\FA16";
+}
+.mdi-subway::before {
+ content: "\F6AB";
+}
+.mdi-subway-alert-variant::before {
+ content: "\FD79";
+}
+.mdi-subway-variant::before {
+ content: "\F4DF";
+}
+.mdi-summit::before {
+ content: "\F785";
+}
+.mdi-sunglasses::before {
+ content: "\F4E0";
+}
+.mdi-surround-sound::before {
+ content: "\F5C5";
+}
+.mdi-surround-sound-2-0::before {
+ content: "\F7EF";
+}
+.mdi-surround-sound-3-1::before {
+ content: "\F7F0";
+}
+.mdi-surround-sound-5-1::before {
+ content: "\F7F1";
+}
+.mdi-surround-sound-7-1::before {
+ content: "\F7F2";
+}
+.mdi-svg::before {
+ content: "\F720";
+}
+.mdi-swap-horizontal::before {
+ content: "\F4E1";
+}
+.mdi-swap-horizontal-bold::before {
+ content: "\FBA9";
+}
+.mdi-swap-horizontal-circle::before {
+ content: "\F0002";
+}
+.mdi-swap-horizontal-circle-outline::before {
+ content: "\F0003";
+}
+.mdi-swap-horizontal-variant::before {
+ content: "\F8C0";
+}
+.mdi-swap-vertical::before {
+ content: "\F4E2";
+}
+.mdi-swap-vertical-bold::before {
+ content: "\FBAA";
+}
+.mdi-swap-vertical-circle::before {
+ content: "\F0004";
+}
+.mdi-swap-vertical-circle-outline::before {
+ content: "\F0005";
+}
+.mdi-swap-vertical-variant::before {
+ content: "\F8C1";
+}
+.mdi-swim::before {
+ content: "\F4E3";
+}
+.mdi-switch::before {
+ content: "\F4E4";
+}
+.mdi-sword::before {
+ content: "\F4E5";
+}
+.mdi-sword-cross::before {
+ content: "\F786";
+}
+.mdi-syllabary-hangul::before {
+ content: "\F035E";
+}
+.mdi-syllabary-hiragana::before {
+ content: "\F035F";
+}
+.mdi-syllabary-katakana::before {
+ content: "\F0360";
+}
+.mdi-syllabary-katakana-half-width::before {
+ content: "\F0361";
+}
+.mdi-symfony::before {
+ content: "\FAE5";
+}
+.mdi-sync::before {
+ content: "\F4E6";
+}
+.mdi-sync-alert::before {
+ content: "\F4E7";
+}
+.mdi-sync-circle::before {
+ content: "\F03A3";
+}
+.mdi-sync-off::before {
+ content: "\F4E8";
+}
+.mdi-tab::before {
+ content: "\F4E9";
+}
+.mdi-tab-minus::before {
+ content: "\FB26";
+}
+.mdi-tab-plus::before {
+ content: "\F75B";
+}
+.mdi-tab-remove::before {
+ content: "\FB27";
+}
+.mdi-tab-unselected::before {
+ content: "\F4EA";
+}
+.mdi-table::before {
+ content: "\F4EB";
+}
+.mdi-table-border::before {
+ content: "\FA17";
+}
+.mdi-table-chair::before {
+ content: "\F0083";
+}
+.mdi-table-column::before {
+ content: "\F834";
+}
+.mdi-table-column-plus-after::before {
+ content: "\F4EC";
+}
+.mdi-table-column-plus-before::before {
+ content: "\F4ED";
+}
+.mdi-table-column-remove::before {
+ content: "\F4EE";
+}
+.mdi-table-column-width::before {
+ content: "\F4EF";
+}
+.mdi-table-edit::before {
+ content: "\F4F0";
+}
+.mdi-table-eye::before {
+ content: "\F00BF";
+}
+.mdi-table-headers-eye::before {
+ content: "\F0248";
+}
+.mdi-table-headers-eye-off::before {
+ content: "\F0249";
+}
+.mdi-table-large::before {
+ content: "\F4F1";
+}
+.mdi-table-large-plus::before {
+ content: "\FFA4";
+}
+.mdi-table-large-remove::before {
+ content: "\FFA5";
+}
+.mdi-table-merge-cells::before {
+ content: "\F9A5";
+}
+.mdi-table-of-contents::before {
+ content: "\F835";
+}
+.mdi-table-plus::before {
+ content: "\FA74";
+}
+.mdi-table-remove::before {
+ content: "\FA75";
+}
+.mdi-table-row::before {
+ content: "\F836";
+}
+.mdi-table-row-height::before {
+ content: "\F4F2";
+}
+.mdi-table-row-plus-after::before {
+ content: "\F4F3";
+}
+.mdi-table-row-plus-before::before {
+ content: "\F4F4";
+}
+.mdi-table-row-remove::before {
+ content: "\F4F5";
+}
+.mdi-table-search::before {
+ content: "\F90E";
+}
+.mdi-table-settings::before {
+ content: "\F837";
+}
+.mdi-table-tennis::before {
+ content: "\FE4B";
+}
+.mdi-tablet::before {
+ content: "\F4F6";
+}
+.mdi-tablet-android::before {
+ content: "\F4F7";
+}
+.mdi-tablet-cellphone::before {
+ content: "\F9A6";
+}
+.mdi-tablet-dashboard::before {
+ content: "\FEEB";
+}
+.mdi-tablet-ipad::before {
+ content: "\F4F8";
+}
+.mdi-taco::before {
+ content: "\F761";
+}
+.mdi-tag::before {
+ content: "\F4F9";
+}
+.mdi-tag-faces::before {
+ content: "\F4FA";
+}
+.mdi-tag-heart::before {
+ content: "\F68A";
+}
+.mdi-tag-heart-outline::before {
+ content: "\FBAB";
+}
+.mdi-tag-minus::before {
+ content: "\F90F";
+}
+.mdi-tag-minus-outline::before {
+ content: "\F024A";
+}
+.mdi-tag-multiple::before {
+ content: "\F4FB";
+}
+.mdi-tag-multiple-outline::before {
+ content: "\F0322";
+}
+.mdi-tag-off::before {
+ content: "\F024B";
+}
+.mdi-tag-off-outline::before {
+ content: "\F024C";
+}
+.mdi-tag-outline::before {
+ content: "\F4FC";
+}
+.mdi-tag-plus::before {
+ content: "\F721";
+}
+.mdi-tag-plus-outline::before {
+ content: "\F024D";
+}
+.mdi-tag-remove::before {
+ content: "\F722";
+}
+.mdi-tag-remove-outline::before {
+ content: "\F024E";
+}
+.mdi-tag-text::before {
+ content: "\F024F";
+}
+.mdi-tag-text-outline::before {
+ content: "\F4FD";
+}
+.mdi-tank::before {
+ content: "\FD16";
+}
+.mdi-tanker-truck::before {
+ content: "\F0006";
+}
+.mdi-tape-measure::before {
+ content: "\FB28";
+}
+.mdi-target::before {
+ content: "\F4FE";
+}
+.mdi-target-account::before {
+ content: "\FBAC";
+}
+.mdi-target-variant::before {
+ content: "\FA76";
+}
+.mdi-taxi::before {
+ content: "\F4FF";
+}
+.mdi-tea::before {
+ content: "\FD7A";
+}
+.mdi-tea-outline::before {
+ content: "\FD7B";
+}
+.mdi-teach::before {
+ content: "\F88F";
+}
+.mdi-teamviewer::before {
+ content: "\F500";
+}
+.mdi-telegram::before {
+ content: "\F501";
+}
+.mdi-telescope::before {
+ content: "\FB29";
+}
+.mdi-television::before {
+ content: "\F502";
+}
+.mdi-television-ambient-light::before {
+ content: "\F0381";
+}
+.mdi-television-box::before {
+ content: "\F838";
+}
+.mdi-television-classic::before {
+ content: "\F7F3";
+}
+.mdi-television-classic-off::before {
+ content: "\F839";
+}
+.mdi-television-clean::before {
+ content: "\F013B";
+}
+.mdi-television-guide::before {
+ content: "\F503";
+}
+.mdi-television-off::before {
+ content: "\F83A";
+}
+.mdi-television-pause::before {
+ content: "\FFA6";
+}
+.mdi-television-play::before {
+ content: "\FEEC";
+}
+.mdi-television-stop::before {
+ content: "\FFA7";
+}
+.mdi-temperature-celsius::before {
+ content: "\F504";
+}
+.mdi-temperature-fahrenheit::before {
+ content: "\F505";
+}
+.mdi-temperature-kelvin::before {
+ content: "\F506";
+}
+.mdi-tennis::before {
+ content: "\FD7C";
+}
+.mdi-tennis-ball::before {
+ content: "\F507";
+}
+.mdi-tent::before {
+ content: "\F508";
+}
+.mdi-terraform::before {
+ content: "\F0084";
+}
+.mdi-terrain::before {
+ content: "\F509";
+}
+.mdi-test-tube::before {
+ content: "\F668";
+}
+.mdi-test-tube-empty::before {
+ content: "\F910";
+}
+.mdi-test-tube-off::before {
+ content: "\F911";
+}
+.mdi-text::before {
+ content: "\F9A7";
+}
+.mdi-text-recognition::before {
+ content: "\F0168";
+}
+.mdi-text-shadow::before {
+ content: "\F669";
+}
+.mdi-text-short::before {
+ content: "\F9A8";
+}
+.mdi-text-subject::before {
+ content: "\F9A9";
+}
+.mdi-text-to-speech::before {
+ content: "\F50A";
+}
+.mdi-text-to-speech-off::before {
+ content: "\F50B";
+}
+.mdi-textarea::before {
+ content: "\F00C0";
+}
+.mdi-textbox::before {
+ content: "\F60E";
+}
+.mdi-textbox-lock::before {
+ content: "\F0388";
+}
+.mdi-textbox-password::before {
+ content: "\F7F4";
+}
+.mdi-texture::before {
+ content: "\F50C";
+}
+.mdi-texture-box::before {
+ content: "\F0007";
+}
+.mdi-theater::before {
+ content: "\F50D";
+}
+.mdi-theme-light-dark::before {
+ content: "\F50E";
+}
+.mdi-thermometer::before {
+ content: "\F50F";
+}
+.mdi-thermometer-alert::before {
+ content: "\FE61";
+}
+.mdi-thermometer-chevron-down::before {
+ content: "\FE62";
+}
+.mdi-thermometer-chevron-up::before {
+ content: "\FE63";
+}
+.mdi-thermometer-high::before {
+ content: "\F00ED";
+}
+.mdi-thermometer-lines::before {
+ content: "\F510";
+}
+.mdi-thermometer-low::before {
+ content: "\F00EE";
+}
+.mdi-thermometer-minus::before {
+ content: "\FE64";
+}
+.mdi-thermometer-plus::before {
+ content: "\FE65";
+}
+.mdi-thermostat::before {
+ content: "\F393";
+}
+.mdi-thermostat-box::before {
+ content: "\F890";
+}
+.mdi-thought-bubble::before {
+ content: "\F7F5";
+}
+.mdi-thought-bubble-outline::before {
+ content: "\F7F6";
+}
+.mdi-thumb-down::before {
+ content: "\F511";
+}
+.mdi-thumb-down-outline::before {
+ content: "\F512";
+}
+.mdi-thumb-up::before {
+ content: "\F513";
+}
+.mdi-thumb-up-outline::before {
+ content: "\F514";
+}
+.mdi-thumbs-up-down::before {
+ content: "\F515";
+}
+.mdi-ticket::before {
+ content: "\F516";
+}
+.mdi-ticket-account::before {
+ content: "\F517";
+}
+.mdi-ticket-confirmation::before {
+ content: "\F518";
+}
+.mdi-ticket-outline::before {
+ content: "\F912";
+}
+.mdi-ticket-percent::before {
+ content: "\F723";
+}
+.mdi-tie::before {
+ content: "\F519";
+}
+.mdi-tilde::before {
+ content: "\F724";
+}
+.mdi-timelapse::before {
+ content: "\F51A";
+}
+.mdi-timeline::before {
+ content: "\FBAD";
+}
+.mdi-timeline-alert::before {
+ content: "\FFB2";
+}
+.mdi-timeline-alert-outline::before {
+ content: "\FFB5";
+}
+.mdi-timeline-clock::before {
+ content: "\F0226";
+}
+.mdi-timeline-clock-outline::before {
+ content: "\F0227";
+}
+.mdi-timeline-help::before {
+ content: "\FFB6";
+}
+.mdi-timeline-help-outline::before {
+ content: "\FFB7";
+}
+.mdi-timeline-outline::before {
+ content: "\FBAE";
+}
+.mdi-timeline-plus::before {
+ content: "\FFB3";
+}
+.mdi-timeline-plus-outline::before {
+ content: "\FFB4";
+}
+.mdi-timeline-text::before {
+ content: "\FBAF";
+}
+.mdi-timeline-text-outline::before {
+ content: "\FBB0";
+}
+.mdi-timer::before {
+ content: "\F51B";
+}
+.mdi-timer-10::before {
+ content: "\F51C";
+}
+.mdi-timer-3::before {
+ content: "\F51D";
+}
+.mdi-timer-off::before {
+ content: "\F51E";
+}
+.mdi-timer-sand::before {
+ content: "\F51F";
+}
+.mdi-timer-sand-empty::before {
+ content: "\F6AC";
+}
+.mdi-timer-sand-full::before {
+ content: "\F78B";
+}
+.mdi-timetable::before {
+ content: "\F520";
+}
+.mdi-toaster::before {
+ content: "\F0085";
+}
+.mdi-toaster-off::before {
+ content: "\F01E2";
+}
+.mdi-toaster-oven::before {
+ content: "\FCAF";
+}
+.mdi-toggle-switch::before {
+ content: "\F521";
+}
+.mdi-toggle-switch-off::before {
+ content: "\F522";
+}
+.mdi-toggle-switch-off-outline::before {
+ content: "\FA18";
+}
+.mdi-toggle-switch-outline::before {
+ content: "\FA19";
+}
+.mdi-toilet::before {
+ content: "\F9AA";
+}
+.mdi-toolbox::before {
+ content: "\F9AB";
+}
+.mdi-toolbox-outline::before {
+ content: "\F9AC";
+}
+.mdi-tools::before {
+ content: "\F0086";
+}
+.mdi-tooltip::before {
+ content: "\F523";
+}
+.mdi-tooltip-account::before {
+ content: "\F00C";
+}
+.mdi-tooltip-edit::before {
+ content: "\F524";
+}
+.mdi-tooltip-edit-outline::before {
+ content: "\F02F0";
+}
+.mdi-tooltip-image::before {
+ content: "\F525";
+}
+.mdi-tooltip-image-outline::before {
+ content: "\FBB1";
+}
+.mdi-tooltip-outline::before {
+ content: "\F526";
+}
+.mdi-tooltip-plus::before {
+ content: "\FBB2";
+}
+.mdi-tooltip-plus-outline::before {
+ content: "\F527";
+}
+.mdi-tooltip-text::before {
+ content: "\F528";
+}
+.mdi-tooltip-text-outline::before {
+ content: "\FBB3";
+}
+.mdi-tooth::before {
+ content: "\F8C2";
+}
+.mdi-tooth-outline::before {
+ content: "\F529";
+}
+.mdi-toothbrush::before {
+ content: "\F0154";
+}
+.mdi-toothbrush-electric::before {
+ content: "\F0157";
+}
+.mdi-toothbrush-paste::before {
+ content: "\F0155";
+}
+.mdi-tor::before {
+ content: "\F52A";
+}
+.mdi-tortoise::before {
+ content: "\FD17";
+}
+.mdi-toslink::before {
+ content: "\F02E3";
+}
+.mdi-tournament::before {
+ content: "\F9AD";
+}
+.mdi-tower-beach::before {
+ content: "\F680";
+}
+.mdi-tower-fire::before {
+ content: "\F681";
+}
+.mdi-towing::before {
+ content: "\F83B";
+}
+.mdi-toy-brick::before {
+ content: "\F02B3";
+}
+.mdi-toy-brick-marker::before {
+ content: "\F02B4";
+}
+.mdi-toy-brick-marker-outline::before {
+ content: "\F02B5";
+}
+.mdi-toy-brick-minus::before {
+ content: "\F02B6";
+}
+.mdi-toy-brick-minus-outline::before {
+ content: "\F02B7";
+}
+.mdi-toy-brick-outline::before {
+ content: "\F02B8";
+}
+.mdi-toy-brick-plus::before {
+ content: "\F02B9";
+}
+.mdi-toy-brick-plus-outline::before {
+ content: "\F02BA";
+}
+.mdi-toy-brick-remove::before {
+ content: "\F02BB";
+}
+.mdi-toy-brick-remove-outline::before {
+ content: "\F02BC";
+}
+.mdi-toy-brick-search::before {
+ content: "\F02BD";
+}
+.mdi-toy-brick-search-outline::before {
+ content: "\F02BE";
+}
+.mdi-track-light::before {
+ content: "\F913";
+}
+.mdi-trackpad::before {
+ content: "\F7F7";
+}
+.mdi-trackpad-lock::before {
+ content: "\F932";
+}
+.mdi-tractor::before {
+ content: "\F891";
+}
+.mdi-trademark::before {
+ content: "\FA77";
+}
+.mdi-traffic-cone::before {
+ content: "\F03A7";
+}
+.mdi-traffic-light::before {
+ content: "\F52B";
+}
+.mdi-train::before {
+ content: "\F52C";
+}
+.mdi-train-car::before {
+ content: "\FBB4";
+}
+.mdi-train-variant::before {
+ content: "\F8C3";
+}
+.mdi-tram::before {
+ content: "\F52D";
+}
+.mdi-tram-side::before {
+ content: "\F0008";
+}
+.mdi-transcribe::before {
+ content: "\F52E";
+}
+.mdi-transcribe-close::before {
+ content: "\F52F";
+}
+.mdi-transfer::before {
+ content: "\F0087";
+}
+.mdi-transfer-down::before {
+ content: "\FD7D";
+}
+.mdi-transfer-left::before {
+ content: "\FD7E";
+}
+.mdi-transfer-right::before {
+ content: "\F530";
+}
+.mdi-transfer-up::before {
+ content: "\FD7F";
+}
+.mdi-transit-connection::before {
+ content: "\FD18";
+}
+.mdi-transit-connection-variant::before {
+ content: "\FD19";
+}
+.mdi-transit-detour::before {
+ content: "\FFA8";
+}
+.mdi-transit-transfer::before {
+ content: "\F6AD";
+}
+.mdi-transition::before {
+ content: "\F914";
+}
+.mdi-transition-masked::before {
+ content: "\F915";
+}
+.mdi-translate::before {
+ content: "\F5CA";
+}
+.mdi-translate-off::before {
+ content: "\FE66";
+}
+.mdi-transmission-tower::before {
+ content: "\FD1A";
+}
+.mdi-trash-can::before {
+ content: "\FA78";
+}
+.mdi-trash-can-outline::before {
+ content: "\FA79";
+}
+.mdi-tray::before {
+ content: "\F02BF";
+}
+.mdi-tray-alert::before {
+ content: "\F02C0";
+}
+.mdi-tray-full::before {
+ content: "\F02C1";
+}
+.mdi-tray-minus::before {
+ content: "\F02C2";
+}
+.mdi-tray-plus::before {
+ content: "\F02C3";
+}
+.mdi-tray-remove::before {
+ content: "\F02C4";
+}
+.mdi-treasure-chest::before {
+ content: "\F725";
+}
+.mdi-tree::before {
+ content: "\F531";
+}
+.mdi-tree-outline::before {
+ content: "\FE4C";
+}
+.mdi-trello::before {
+ content: "\F532";
+}
+.mdi-trending-down::before {
+ content: "\F533";
+}
+.mdi-trending-neutral::before {
+ content: "\F534";
+}
+.mdi-trending-up::before {
+ content: "\F535";
+}
+.mdi-triangle::before {
+ content: "\F536";
+}
+.mdi-triangle-outline::before {
+ content: "\F537";
+}
+.mdi-triforce::before {
+ content: "\FBB5";
+}
+.mdi-trophy::before {
+ content: "\F538";
+}
+.mdi-trophy-award::before {
+ content: "\F539";
+}
+.mdi-trophy-broken::before {
+ content: "\FD80";
+}
+.mdi-trophy-outline::before {
+ content: "\F53A";
+}
+.mdi-trophy-variant::before {
+ content: "\F53B";
+}
+.mdi-trophy-variant-outline::before {
+ content: "\F53C";
+}
+.mdi-truck::before {
+ content: "\F53D";
+}
+.mdi-truck-check::before {
+ content: "\FCB0";
+}
+.mdi-truck-check-outline::before {
+ content: "\F02C5";
+}
+.mdi-truck-delivery::before {
+ content: "\F53E";
+}
+.mdi-truck-delivery-outline::before {
+ content: "\F02C6";
+}
+.mdi-truck-fast::before {
+ content: "\F787";
+}
+.mdi-truck-fast-outline::before {
+ content: "\F02C7";
+}
+.mdi-truck-outline::before {
+ content: "\F02C8";
+}
+.mdi-truck-trailer::before {
+ content: "\F726";
+}
+.mdi-trumpet::before {
+ content: "\F00C1";
+}
+.mdi-tshirt-crew::before {
+ content: "\FA7A";
+}
+.mdi-tshirt-crew-outline::before {
+ content: "\F53F";
+}
+.mdi-tshirt-v::before {
+ content: "\FA7B";
+}
+.mdi-tshirt-v-outline::before {
+ content: "\F540";
+}
+.mdi-tumble-dryer::before {
+ content: "\F916";
+}
+.mdi-tumble-dryer-alert::before {
+ content: "\F01E5";
+}
+.mdi-tumble-dryer-off::before {
+ content: "\F01E6";
+}
+.mdi-tumblr::before {
+ content: "\F541";
+}
+.mdi-tumblr-box::before {
+ content: "\F917";
+}
+.mdi-tumblr-reblog::before {
+ content: "\F542";
+}
+.mdi-tune::before {
+ content: "\F62E";
+}
+.mdi-tune-vertical::before {
+ content: "\F66A";
+}
+.mdi-turnstile::before {
+ content: "\FCB1";
+}
+.mdi-turnstile-outline::before {
+ content: "\FCB2";
+}
+.mdi-turtle::before {
+ content: "\FCB3";
+}
+.mdi-twitch::before {
+ content: "\F543";
+}
+.mdi-twitter::before {
+ content: "\F544";
+}
+.mdi-twitter-box::before {
+ content: "\F545";
+}
+.mdi-twitter-circle::before {
+ content: "\F546";
+}
+.mdi-twitter-retweet::before {
+ content: "\F547";
+}
+.mdi-two-factor-authentication::before {
+ content: "\F9AE";
+}
+.mdi-typewriter::before {
+ content: "\FF4A";
+}
+.mdi-uber::before {
+ content: "\F748";
+}
+.mdi-ubisoft::before {
+ content: "\FBB6";
+}
+.mdi-ubuntu::before {
+ content: "\F548";
+}
+.mdi-ufo::before {
+ content: "\F00EF";
+}
+.mdi-ufo-outline::before {
+ content: "\F00F0";
+}
+.mdi-ultra-high-definition::before {
+ content: "\F7F8";
+}
+.mdi-umbraco::before {
+ content: "\F549";
+}
+.mdi-umbrella::before {
+ content: "\F54A";
+}
+.mdi-umbrella-closed::before {
+ content: "\F9AF";
+}
+.mdi-umbrella-outline::before {
+ content: "\F54B";
+}
+.mdi-undo::before {
+ content: "\F54C";
+}
+.mdi-undo-variant::before {
+ content: "\F54D";
+}
+.mdi-unfold-less-horizontal::before {
+ content: "\F54E";
+}
+.mdi-unfold-less-vertical::before {
+ content: "\F75F";
+}
+.mdi-unfold-more-horizontal::before {
+ content: "\F54F";
+}
+.mdi-unfold-more-vertical::before {
+ content: "\F760";
+}
+.mdi-ungroup::before {
+ content: "\F550";
+}
+.mdi-unicode::before {
+ content: "\FEED";
+}
+.mdi-unity::before {
+ content: "\F6AE";
+}
+.mdi-unreal::before {
+ content: "\F9B0";
+}
+.mdi-untappd::before {
+ content: "\F551";
+}
+.mdi-update::before {
+ content: "\F6AF";
+}
+.mdi-upload::before {
+ content: "\F552";
+}
+.mdi-upload-lock::before {
+ content: "\F039E";
+}
+.mdi-upload-lock-outline::before {
+ content: "\F039F";
+}
+.mdi-upload-multiple::before {
+ content: "\F83C";
+}
+.mdi-upload-network::before {
+ content: "\F6F5";
+}
+.mdi-upload-network-outline::before {
+ content: "\FCB4";
+}
+.mdi-upload-off::before {
+ content: "\F00F1";
+}
+.mdi-upload-off-outline::before {
+ content: "\F00F2";
+}
+.mdi-upload-outline::before {
+ content: "\FE67";
+}
+.mdi-usb::before {
+ content: "\F553";
+}
+.mdi-usb-flash-drive::before {
+ content: "\F02C9";
+}
+.mdi-usb-flash-drive-outline::before {
+ content: "\F02CA";
+}
+.mdi-usb-port::before {
+ content: "\F021B";
+}
+.mdi-valve::before {
+ content: "\F0088";
+}
+.mdi-valve-closed::before {
+ content: "\F0089";
+}
+.mdi-valve-open::before {
+ content: "\F008A";
+}
+.mdi-van-passenger::before {
+ content: "\F7F9";
+}
+.mdi-van-utility::before {
+ content: "\F7FA";
+}
+.mdi-vanish::before {
+ content: "\F7FB";
+}
+.mdi-vanity-light::before {
+ content: "\F020C";
+}
+.mdi-variable::before {
+ content: "\FAE6";
+}
+.mdi-variable-box::before {
+ content: "\F013C";
+}
+.mdi-vector-arrange-above::before {
+ content: "\F554";
+}
+.mdi-vector-arrange-below::before {
+ content: "\F555";
+}
+.mdi-vector-bezier::before {
+ content: "\FAE7";
+}
+.mdi-vector-circle::before {
+ content: "\F556";
+}
+.mdi-vector-circle-variant::before {
+ content: "\F557";
+}
+.mdi-vector-combine::before {
+ content: "\F558";
+}
+.mdi-vector-curve::before {
+ content: "\F559";
+}
+.mdi-vector-difference::before {
+ content: "\F55A";
+}
+.mdi-vector-difference-ab::before {
+ content: "\F55B";
+}
+.mdi-vector-difference-ba::before {
+ content: "\F55C";
+}
+.mdi-vector-ellipse::before {
+ content: "\F892";
+}
+.mdi-vector-intersection::before {
+ content: "\F55D";
+}
+.mdi-vector-line::before {
+ content: "\F55E";
+}
+.mdi-vector-link::before {
+ content: "\F0009";
+}
+.mdi-vector-point::before {
+ content: "\F55F";
+}
+.mdi-vector-polygon::before {
+ content: "\F560";
+}
+.mdi-vector-polyline::before {
+ content: "\F561";
+}
+.mdi-vector-polyline-edit::before {
+ content: "\F0250";
+}
+.mdi-vector-polyline-minus::before {
+ content: "\F0251";
+}
+.mdi-vector-polyline-plus::before {
+ content: "\F0252";
+}
+.mdi-vector-polyline-remove::before {
+ content: "\F0253";
+}
+.mdi-vector-radius::before {
+ content: "\F749";
+}
+.mdi-vector-rectangle::before {
+ content: "\F5C6";
+}
+.mdi-vector-selection::before {
+ content: "\F562";
+}
+.mdi-vector-square::before {
+ content: "\F001";
+}
+.mdi-vector-triangle::before {
+ content: "\F563";
+}
+.mdi-vector-union::before {
+ content: "\F564";
+}
+.mdi-venmo::before {
+ content: "\F578";
+}
+.mdi-vhs::before {
+ content: "\FA1A";
+}
+.mdi-vibrate::before {
+ content: "\F566";
+}
+.mdi-vibrate-off::before {
+ content: "\FCB5";
+}
+.mdi-video::before {
+ content: "\F567";
+}
+.mdi-video-3d::before {
+ content: "\F7FC";
+}
+.mdi-video-3d-variant::before {
+ content: "\FEEE";
+}
+.mdi-video-4k-box::before {
+ content: "\F83D";
+}
+.mdi-video-account::before {
+ content: "\F918";
+}
+.mdi-video-check::before {
+ content: "\F008B";
+}
+.mdi-video-check-outline::before {
+ content: "\F008C";
+}
+.mdi-video-image::before {
+ content: "\F919";
+}
+.mdi-video-input-antenna::before {
+ content: "\F83E";
+}
+.mdi-video-input-component::before {
+ content: "\F83F";
+}
+.mdi-video-input-hdmi::before {
+ content: "\F840";
+}
+.mdi-video-input-scart::before {
+ content: "\FFA9";
+}
+.mdi-video-input-svideo::before {
+ content: "\F841";
+}
+.mdi-video-minus::before {
+ content: "\F9B1";
+}
+.mdi-video-off::before {
+ content: "\F568";
+}
+.mdi-video-off-outline::before {
+ content: "\FBB7";
+}
+.mdi-video-outline::before {
+ content: "\FBB8";
+}
+.mdi-video-plus::before {
+ content: "\F9B2";
+}
+.mdi-video-stabilization::before {
+ content: "\F91A";
+}
+.mdi-video-switch::before {
+ content: "\F569";
+}
+.mdi-video-vintage::before {
+ content: "\FA1B";
+}
+.mdi-video-wireless::before {
+ content: "\FEEF";
+}
+.mdi-video-wireless-outline::before {
+ content: "\FEF0";
+}
+.mdi-view-agenda::before {
+ content: "\F56A";
+}
+.mdi-view-agenda-outline::before {
+ content: "\F0203";
+}
+.mdi-view-array::before {
+ content: "\F56B";
+}
+.mdi-view-carousel::before {
+ content: "\F56C";
+}
+.mdi-view-column::before {
+ content: "\F56D";
+}
+.mdi-view-comfy::before {
+ content: "\FE4D";
+}
+.mdi-view-compact::before {
+ content: "\FE4E";
+}
+.mdi-view-compact-outline::before {
+ content: "\FE4F";
+}
+.mdi-view-dashboard::before {
+ content: "\F56E";
+}
+.mdi-view-dashboard-outline::before {
+ content: "\FA1C";
+}
+.mdi-view-dashboard-variant::before {
+ content: "\F842";
+}
+.mdi-view-day::before {
+ content: "\F56F";
+}
+.mdi-view-grid::before {
+ content: "\F570";
+}
+.mdi-view-grid-outline::before {
+ content: "\F0204";
+}
+.mdi-view-grid-plus::before {
+ content: "\FFAA";
+}
+.mdi-view-grid-plus-outline::before {
+ content: "\F0205";
+}
+.mdi-view-headline::before {
+ content: "\F571";
+}
+.mdi-view-list::before {
+ content: "\F572";
+}
+.mdi-view-module::before {
+ content: "\F573";
+}
+.mdi-view-parallel::before {
+ content: "\F727";
+}
+.mdi-view-quilt::before {
+ content: "\F574";
+}
+.mdi-view-sequential::before {
+ content: "\F728";
+}
+.mdi-view-split-horizontal::before {
+ content: "\FBA7";
+}
+.mdi-view-split-vertical::before {
+ content: "\FBA8";
+}
+.mdi-view-stream::before {
+ content: "\F575";
+}
+.mdi-view-week::before {
+ content: "\F576";
+}
+.mdi-vimeo::before {
+ content: "\F577";
+}
+.mdi-violin::before {
+ content: "\F60F";
+}
+.mdi-virtual-reality::before {
+ content: "\F893";
+}
+.mdi-visual-studio::before {
+ content: "\F610";
+}
+.mdi-visual-studio-code::before {
+ content: "\FA1D";
+}
+.mdi-vk::before {
+ content: "\F579";
+}
+.mdi-vk-box::before {
+ content: "\F57A";
+}
+.mdi-vk-circle::before {
+ content: "\F57B";
+}
+.mdi-vlc::before {
+ content: "\F57C";
+}
+.mdi-voice::before {
+ content: "\F5CB";
+}
+.mdi-voice-off::before {
+ content: "\FEF1";
+}
+.mdi-voicemail::before {
+ content: "\F57D";
+}
+.mdi-volleyball::before {
+ content: "\F9B3";
+}
+.mdi-volume-high::before {
+ content: "\F57E";
+}
+.mdi-volume-low::before {
+ content: "\F57F";
+}
+.mdi-volume-medium::before {
+ content: "\F580";
+}
+.mdi-volume-minus::before {
+ content: "\F75D";
+}
+.mdi-volume-mute::before {
+ content: "\F75E";
+}
+.mdi-volume-off::before {
+ content: "\F581";
+}
+.mdi-volume-plus::before {
+ content: "\F75C";
+}
+.mdi-volume-source::before {
+ content: "\F014B";
+}
+.mdi-volume-variant-off::before {
+ content: "\FE68";
+}
+.mdi-volume-vibrate::before {
+ content: "\F014C";
+}
+.mdi-vote::before {
+ content: "\FA1E";
+}
+.mdi-vote-outline::before {
+ content: "\FA1F";
+}
+.mdi-vpn::before {
+ content: "\F582";
+}
+.mdi-vuejs::before {
+ content: "\F843";
+}
+.mdi-vuetify::before {
+ content: "\FE50";
+}
+.mdi-walk::before {
+ content: "\F583";
+}
+.mdi-wall::before {
+ content: "\F7FD";
+}
+.mdi-wall-sconce::before {
+ content: "\F91B";
+}
+.mdi-wall-sconce-flat::before {
+ content: "\F91C";
+}
+.mdi-wall-sconce-variant::before {
+ content: "\F91D";
+}
+.mdi-wallet::before {
+ content: "\F584";
+}
+.mdi-wallet-giftcard::before {
+ content: "\F585";
+}
+.mdi-wallet-membership::before {
+ content: "\F586";
+}
+.mdi-wallet-outline::before {
+ content: "\FBB9";
+}
+.mdi-wallet-plus::before {
+ content: "\FFAB";
+}
+.mdi-wallet-plus-outline::before {
+ content: "\FFAC";
+}
+.mdi-wallet-travel::before {
+ content: "\F587";
+}
+.mdi-wallpaper::before {
+ content: "\FE69";
+}
+.mdi-wan::before {
+ content: "\F588";
+}
+.mdi-wardrobe::before {
+ content: "\FFAD";
+}
+.mdi-wardrobe-outline::before {
+ content: "\FFAE";
+}
+.mdi-warehouse::before {
+ content: "\FFBB";
+}
+.mdi-washing-machine::before {
+ content: "\F729";
+}
+.mdi-washing-machine-alert::before {
+ content: "\F01E7";
+}
+.mdi-washing-machine-off::before {
+ content: "\F01E8";
+}
+.mdi-watch::before {
+ content: "\F589";
+}
+.mdi-watch-export::before {
+ content: "\F58A";
+}
+.mdi-watch-export-variant::before {
+ content: "\F894";
+}
+.mdi-watch-import::before {
+ content: "\F58B";
+}
+.mdi-watch-import-variant::before {
+ content: "\F895";
+}
+.mdi-watch-variant::before {
+ content: "\F896";
+}
+.mdi-watch-vibrate::before {
+ content: "\F6B0";
+}
+.mdi-watch-vibrate-off::before {
+ content: "\FCB6";
+}
+.mdi-water::before {
+ content: "\F58C";
+}
+.mdi-water-boiler::before {
+ content: "\FFAF";
+}
+.mdi-water-boiler-alert::before {
+ content: "\F01DE";
+}
+.mdi-water-boiler-off::before {
+ content: "\F01DF";
+}
+.mdi-water-off::before {
+ content: "\F58D";
+}
+.mdi-water-outline::before {
+ content: "\FE6A";
+}
+.mdi-water-percent::before {
+ content: "\F58E";
+}
+.mdi-water-polo::before {
+ content: "\F02CB";
+}
+.mdi-water-pump::before {
+ content: "\F58F";
+}
+.mdi-water-pump-off::before {
+ content: "\FFB0";
+}
+.mdi-water-well::before {
+ content: "\F008D";
+}
+.mdi-water-well-outline::before {
+ content: "\F008E";
+}
+.mdi-watermark::before {
+ content: "\F612";
+}
+.mdi-wave::before {
+ content: "\FF4B";
+}
+.mdi-waves::before {
+ content: "\F78C";
+}
+.mdi-waze::before {
+ content: "\FBBA";
+}
+.mdi-weather-cloudy::before {
+ content: "\F590";
+}
+.mdi-weather-cloudy-alert::before {
+ content: "\FF4C";
+}
+.mdi-weather-cloudy-arrow-right::before {
+ content: "\FE51";
+}
+.mdi-weather-fog::before {
+ content: "\F591";
+}
+.mdi-weather-hail::before {
+ content: "\F592";
+}
+.mdi-weather-hazy::before {
+ content: "\FF4D";
+}
+.mdi-weather-hurricane::before {
+ content: "\F897";
+}
+.mdi-weather-lightning::before {
+ content: "\F593";
+}
+.mdi-weather-lightning-rainy::before {
+ content: "\F67D";
+}
+.mdi-weather-night::before {
+ content: "\F594";
+}
+.mdi-weather-night-partly-cloudy::before {
+ content: "\FF4E";
+}
+.mdi-weather-partly-cloudy::before {
+ content: "\F595";
+}
+.mdi-weather-partly-lightning::before {
+ content: "\FF4F";
+}
+.mdi-weather-partly-rainy::before {
+ content: "\FF50";
+}
+.mdi-weather-partly-snowy::before {
+ content: "\FF51";
+}
+.mdi-weather-partly-snowy-rainy::before {
+ content: "\FF52";
+}
+.mdi-weather-pouring::before {
+ content: "\F596";
+}
+.mdi-weather-rainy::before {
+ content: "\F597";
+}
+.mdi-weather-snowy::before {
+ content: "\F598";
+}
+.mdi-weather-snowy-heavy::before {
+ content: "\FF53";
+}
+.mdi-weather-snowy-rainy::before {
+ content: "\F67E";
+}
+.mdi-weather-sunny::before {
+ content: "\F599";
+}
+.mdi-weather-sunny-alert::before {
+ content: "\FF54";
+}
+.mdi-weather-sunset::before {
+ content: "\F59A";
+}
+.mdi-weather-sunset-down::before {
+ content: "\F59B";
+}
+.mdi-weather-sunset-up::before {
+ content: "\F59C";
+}
+.mdi-weather-tornado::before {
+ content: "\FF55";
+}
+.mdi-weather-windy::before {
+ content: "\F59D";
+}
+.mdi-weather-windy-variant::before {
+ content: "\F59E";
+}
+.mdi-web::before {
+ content: "\F59F";
+}
+.mdi-web-box::before {
+ content: "\FFB1";
+}
+.mdi-web-clock::before {
+ content: "\F0275";
+}
+.mdi-webcam::before {
+ content: "\F5A0";
+}
+.mdi-webhook::before {
+ content: "\F62F";
+}
+.mdi-webpack::before {
+ content: "\F72A";
+}
+.mdi-webrtc::before {
+ content: "\F0273";
+}
+.mdi-wechat::before {
+ content: "\F611";
+}
+.mdi-weight::before {
+ content: "\F5A1";
+}
+.mdi-weight-gram::before {
+ content: "\FD1B";
+}
+.mdi-weight-kilogram::before {
+ content: "\F5A2";
+}
+.mdi-weight-lifter::before {
+ content: "\F0188";
+}
+.mdi-weight-pound::before {
+ content: "\F9B4";
+}
+.mdi-whatsapp::before {
+ content: "\F5A3";
+}
+.mdi-wheelchair-accessibility::before {
+ content: "\F5A4";
+}
+.mdi-whistle::before {
+ content: "\F9B5";
+}
+.mdi-whistle-outline::before {
+ content: "\F02E7";
+}
+.mdi-white-balance-auto::before {
+ content: "\F5A5";
+}
+.mdi-white-balance-incandescent::before {
+ content: "\F5A6";
+}
+.mdi-white-balance-iridescent::before {
+ content: "\F5A7";
+}
+.mdi-white-balance-sunny::before {
+ content: "\F5A8";
+}
+.mdi-widgets::before {
+ content: "\F72B";
+}
+.mdi-widgets-outline::before {
+ content: "\F0380";
+}
+.mdi-wifi::before {
+ content: "\F5A9";
+}
+.mdi-wifi-off::before {
+ content: "\F5AA";
+}
+.mdi-wifi-star::before {
+ content: "\FE6B";
+}
+.mdi-wifi-strength-1::before {
+ content: "\F91E";
+}
+.mdi-wifi-strength-1-alert::before {
+ content: "\F91F";
+}
+.mdi-wifi-strength-1-lock::before {
+ content: "\F920";
+}
+.mdi-wifi-strength-2::before {
+ content: "\F921";
+}
+.mdi-wifi-strength-2-alert::before {
+ content: "\F922";
+}
+.mdi-wifi-strength-2-lock::before {
+ content: "\F923";
+}
+.mdi-wifi-strength-3::before {
+ content: "\F924";
+}
+.mdi-wifi-strength-3-alert::before {
+ content: "\F925";
+}
+.mdi-wifi-strength-3-lock::before {
+ content: "\F926";
+}
+.mdi-wifi-strength-4::before {
+ content: "\F927";
+}
+.mdi-wifi-strength-4-alert::before {
+ content: "\F928";
+}
+.mdi-wifi-strength-4-lock::before {
+ content: "\F929";
+}
+.mdi-wifi-strength-alert-outline::before {
+ content: "\F92A";
+}
+.mdi-wifi-strength-lock-outline::before {
+ content: "\F92B";
+}
+.mdi-wifi-strength-off::before {
+ content: "\F92C";
+}
+.mdi-wifi-strength-off-outline::before {
+ content: "\F92D";
+}
+.mdi-wifi-strength-outline::before {
+ content: "\F92E";
+}
+.mdi-wii::before {
+ content: "\F5AB";
+}
+.mdi-wiiu::before {
+ content: "\F72C";
+}
+.mdi-wikipedia::before {
+ content: "\F5AC";
+}
+.mdi-wind-turbine::before {
+ content: "\FD81";
+}
+.mdi-window-close::before {
+ content: "\F5AD";
+}
+.mdi-window-closed::before {
+ content: "\F5AE";
+}
+.mdi-window-closed-variant::before {
+ content: "\F0206";
+}
+.mdi-window-maximize::before {
+ content: "\F5AF";
+}
+.mdi-window-minimize::before {
+ content: "\F5B0";
+}
+.mdi-window-open::before {
+ content: "\F5B1";
+}
+.mdi-window-open-variant::before {
+ content: "\F0207";
+}
+.mdi-window-restore::before {
+ content: "\F5B2";
+}
+.mdi-window-shutter::before {
+ content: "\F0147";
+}
+.mdi-window-shutter-alert::before {
+ content: "\F0148";
+}
+.mdi-window-shutter-open::before {
+ content: "\F0149";
+}
+.mdi-windows::before {
+ content: "\F5B3";
+}
+.mdi-windows-classic::before {
+ content: "\FA20";
+}
+.mdi-wiper::before {
+ content: "\FAE8";
+}
+.mdi-wiper-wash::before {
+ content: "\FD82";
+}
+.mdi-wordpress::before {
+ content: "\F5B4";
+}
+.mdi-worker::before {
+ content: "\F5B5";
+}
+.mdi-wrap::before {
+ content: "\F5B6";
+}
+.mdi-wrap-disabled::before {
+ content: "\FBBB";
+}
+.mdi-wrench::before {
+ content: "\F5B7";
+}
+.mdi-wrench-outline::before {
+ content: "\FBBC";
+}
+.mdi-wunderlist::before {
+ content: "\F5B8";
+}
+.mdi-xamarin::before {
+ content: "\F844";
+}
+.mdi-xamarin-outline::before {
+ content: "\F845";
+}
+.mdi-xaml::before {
+ content: "\F673";
+}
+.mdi-xbox::before {
+ content: "\F5B9";
+}
+.mdi-xbox-controller::before {
+ content: "\F5BA";
+}
+.mdi-xbox-controller-battery-alert::before {
+ content: "\F74A";
+}
+.mdi-xbox-controller-battery-charging::before {
+ content: "\FA21";
+}
+.mdi-xbox-controller-battery-empty::before {
+ content: "\F74B";
+}
+.mdi-xbox-controller-battery-full::before {
+ content: "\F74C";
+}
+.mdi-xbox-controller-battery-low::before {
+ content: "\F74D";
+}
+.mdi-xbox-controller-battery-medium::before {
+ content: "\F74E";
+}
+.mdi-xbox-controller-battery-unknown::before {
+ content: "\F74F";
+}
+.mdi-xbox-controller-menu::before {
+ content: "\FE52";
+}
+.mdi-xbox-controller-off::before {
+ content: "\F5BB";
+}
+.mdi-xbox-controller-view::before {
+ content: "\FE53";
+}
+.mdi-xda::before {
+ content: "\F5BC";
+}
+.mdi-xing::before {
+ content: "\F5BD";
+}
+.mdi-xing-box::before {
+ content: "\F5BE";
+}
+.mdi-xing-circle::before {
+ content: "\F5BF";
+}
+.mdi-xml::before {
+ content: "\F5C0";
+}
+.mdi-xmpp::before {
+ content: "\F7FE";
+}
+.mdi-yahoo::before {
+ content: "\FB2A";
+}
+.mdi-yammer::before {
+ content: "\F788";
+}
+.mdi-yeast::before {
+ content: "\F5C1";
+}
+.mdi-yelp::before {
+ content: "\F5C2";
+}
+.mdi-yin-yang::before {
+ content: "\F67F";
+}
+.mdi-yoga::before {
+ content: "\F01A7";
+}
+.mdi-youtube::before {
+ content: "\F5C3";
+}
+.mdi-youtube-creator-studio::before {
+ content: "\F846";
+}
+.mdi-youtube-gaming::before {
+ content: "\F847";
+}
+.mdi-youtube-subscription::before {
+ content: "\FD1C";
+}
+.mdi-youtube-tv::before {
+ content: "\F448";
+}
+.mdi-z-wave::before {
+ content: "\FAE9";
+}
+.mdi-zend::before {
+ content: "\FAEA";
+}
+.mdi-zigbee::before {
+ content: "\FD1D";
+}
+.mdi-zip-box::before {
+ content: "\F5C4";
+}
+.mdi-zip-box-outline::before {
+ content: "\F001B";
+}
+.mdi-zip-disk::before {
+ content: "\FA22";
+}
+.mdi-zodiac-aquarius::before {
+ content: "\FA7C";
+}
+.mdi-zodiac-aries::before {
+ content: "\FA7D";
+}
+.mdi-zodiac-cancer::before {
+ content: "\FA7E";
+}
+.mdi-zodiac-capricorn::before {
+ content: "\FA7F";
+}
+.mdi-zodiac-gemini::before {
+ content: "\FA80";
+}
+.mdi-zodiac-leo::before {
+ content: "\FA81";
+}
+.mdi-zodiac-libra::before {
+ content: "\FA82";
+}
+.mdi-zodiac-pisces::before {
+ content: "\FA83";
+}
+.mdi-zodiac-sagittarius::before {
+ content: "\FA84";
+}
+.mdi-zodiac-scorpio::before {
+ content: "\FA85";
+}
+.mdi-zodiac-taurus::before {
+ content: "\FA86";
+}
+.mdi-zodiac-virgo::before {
+ content: "\FA87";
+}
+.mdi-blank::before {
+ content: "\F68C";
+ visibility: hidden;
+}
+.mdi-18px.mdi-set,
+.mdi-18px.mdi:before {
+ font-size: 18px;
+}
+.mdi-24px.mdi-set,
+.mdi-24px.mdi:before {
+ font-size: 24px;
+}
+.mdi-36px.mdi-set,
+.mdi-36px.mdi:before {
+ font-size: 36px;
+}
+.mdi-48px.mdi-set,
+.mdi-48px.mdi:before {
+ font-size: 48px;
+}
+.mdi-dark:before {
+ color: rgba(0, 0, 0, 0.54);
+}
+.mdi-dark.mdi-inactive:before {
+ color: rgba(0, 0, 0, 0.26);
+}
+.mdi-light:before {
+ color: #fff;
+}
+.mdi-light.mdi-inactive:before {
+ color: rgba(255, 255, 255, 0.3);
+}
+.mdi-rotate-45:before {
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+.mdi-rotate-90:before {
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.mdi-rotate-135:before {
+ -webkit-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ transform: rotate(135deg);
+}
+.mdi-rotate-180:before {
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
+.mdi-rotate-225:before {
+ -webkit-transform: rotate(225deg);
+ -ms-transform: rotate(225deg);
+ transform: rotate(225deg);
+}
+.mdi-rotate-270:before {
+ -webkit-transform: rotate(270deg);
+ -ms-transform: rotate(270deg);
+ transform: rotate(270deg);
+}
+.mdi-rotate-315:before {
+ -webkit-transform: rotate(315deg);
+ -ms-transform: rotate(315deg);
+ transform: rotate(315deg);
+}
+.mdi-flip-h:before {
+ -webkit-transform: scaleX(-1);
+ transform: scaleX(-1);
+ filter: FlipH;
+ -ms-filter: "FlipH";
+}
+.mdi-flip-v:before {
+ -webkit-transform: scaleY(-1);
+ transform: scaleY(-1);
+ filter: FlipV;
+ -ms-filter: "FlipV";
+}
+.mdi-spin:before {
+ -webkit-animation: mdi-spin 2s infinite linear;
+ animation: mdi-spin 2s infinite linear;
+}
+@-webkit-keyframes mdi-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes mdi-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+/*# sourceMappingURL=materialdesignicons.css.map */
diff --git a/packages/auditor-backoffice-ui/src/scss/libs/_all.scss b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
new file mode 100644
index 000000000..cba6f26eb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
@@ -0,0 +1,29 @@
+/*
+ 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)
+ */
+
+@import "node_modules/bulma-radio/bulma-radio";
+// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
+@import "node_modules/bulma-checkbox/bulma-checkbox";
+@import "node_modules/bulma-switch-control/bulma-switch-control";
+@import "node_modules/bulma-upload-control/bulma-upload-control";
+
+/* Bulma */
+@import "node_modules/bulma/bulma";
diff --git a/packages/auditor-backoffice-ui/src/scss/main.scss b/packages/auditor-backoffice-ui/src/scss/main.scss
new file mode 100644
index 000000000..c4be8aa73
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/main.scss
@@ -0,0 +1,195 @@
+/*
+ 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)
+ */
+
+/* Theme style (colors & sizes) */
+@import "theme-default";
+
+/* Core Libs & Lib configs */
+@import "libs/all";
+
+/* Mixins */
+@import "mixins";
+
+/* Theme components */
+@import "nav-bar";
+@import "aside";
+@import "title-bar";
+@import "hero-bar";
+@import "card";
+@import "table";
+@import "tiles";
+@import "form";
+@import "main-section";
+@import "modal";
+@import "footer";
+@import "misc";
+@import "custom-calendar";
+@import "loading";
+
+@import "fonts/nunito.css";
+@import "icons/materialdesignicons-4.9.95.min.css";
+
+$tooltip-color: red;
+
+@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
+@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+
+@import "toggle";
+
+.notification {
+ background-color: transparent;
+}
+
+.timeline .timeline-item .timeline-content {
+ padding-top: 0;
+}
+
+.timeline .timeline-item:last-child::before {
+ display: none;
+}
+
+.timeline .timeline-item .timeline-marker {
+ top: 0;
+}
+
+.toast {
+ position: absolute;
+ width: 60%;
+ margin-left: 10%;
+ margin-right: 10%;
+ z-index: 999;
+
+ display: flex;
+ flex-direction: column;
+ padding: 15px;
+ text-align: center;
+ pointer-events: none;
+}
+
+.toast>.message {
+ white-space: pre-wrap;
+ opacity: 80%;
+}
+
+div {
+ &.is-loading {
+ position: relative;
+ pointer-events: none;
+ opacity: 0.5;
+
+ &:after {
+ // @include loader;
+ position: absolute;
+ top: calc(50% - 2.5em);
+ left: calc(50% - 2.5em);
+ width: 5em;
+ height: 5em;
+ border-width: 0.25em;
+ }
+ }
+}
+
+input[type="checkbox"]:indeterminate+.check {
+ background: red !important;
+}
+
+.right-sticky {
+ position: sticky;
+ right: 0px;
+ background-color: $white;
+}
+
+.right-sticky .buttons {
+ flex-wrap: nowrap;
+}
+
+.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky {
+ background-color: #fafafa;
+}
+
+tr:hover .right-sticky {
+ background-color: hsl(0, 0%, 80%);
+}
+
+.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
+ background-color: hsl(0, 0%, 95%);
+}
+
+.content-full-size {
+ height: calc(100% - 3rem);
+ position: absolute;
+ width: calc(100% - 14rem);
+ display: flex;
+}
+
+.content-full-size .column .card {
+ min-width: 200px;
+}
+
+@include touch {
+ .content-full-size {
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ }
+}
+
+.column.is-half {
+ flex: none;
+ width: 50%;
+}
+
+input:read-only {
+ cursor: initial;
+}
+
+[data-tooltip]:before {
+ max-width: 15rem;
+ width: max-content;
+ text-align: left;
+ transition: opacity 0.1s linear 1s;
+ // transform: inherit !important;
+ white-space: pre-wrap !important;
+ font-weight: normal;
+ // position: relative;
+}
+
+.icon[data-tooltip]:before {
+ transition: none;
+ z-index: 5;
+}
+
+span[data-tooltip] {
+ border-bottom: none;
+}
+
+div[data-tooltip]::before {
+ position: absolute;
+}
+
+.modal-card-body>p {
+ padding: 1em;
+}
+
+.modal-card-body>p.warning {
+ background-color: #fffbdd;
+ border: solid 1px #f2e9bf;
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/scss/toggle.scss b/packages/auditor-backoffice-ui/src/scss/toggle.scss
new file mode 100644
index 000000000..24636da2f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/scss/toggle.scss
@@ -0,0 +1,51 @@
+$green: #56c080;
+
+.toggle {
+ cursor: pointer;
+ display: inline-block;
+}
+.toggle-switch {
+ display: inline-block;
+ background: #ccc;
+ border-radius: 16px;
+ width: 58px;
+ height: 32px;
+ position: relative;
+ vertical-align: middle;
+ transition: background 0.25s;
+ &:before,
+ &:after {
+ content: "";
+ }
+ &:before {
+ display: block;
+ background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ transition: left 0.25s;
+ }
+ .toggle:hover &:before {
+ background: linear-gradient(to bottom, #fff 0%, #fff 100%);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
+ }
+ .toggle-checkbox:checked + & {
+ background: $green;
+ &:before {
+ left: 30px;
+ }
+ }
+}
+.toggle-checkbox {
+ position: absolute;
+ visibility: hidden;
+}
+.toggle-label {
+ margin-left: 5px;
+ position: relative;
+ top: 2px;
+}
diff --git a/packages/auditor-backoffice-ui/src/stories.test.ts b/packages/auditor-backoffice-ui/src/stories.test.ts
new file mode 100644
index 000000000..abd993550
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/stories.test.ts
@@ -0,0 +1,44 @@
+/*
+ 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)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as admin from "./paths/admin/index.stories.js";
+import * as instance from "./paths/instance/index.stories.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ admin, instance });
+ cms.forEach((group) => {
+ describe(`Example for group: ${group.title}`, () => {
+ group.list.forEach((component) => {
+ describe(`Component: ${component.name}`, () => {
+ component.examples.forEach((example) => {
+ it(`should render example: ${example.name}`, () => {
+ tests.renderUI(example.render);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/stories.tsx b/packages/auditor-backoffice-ui/src/stories.tsx
new file mode 100644
index 000000000..8bb06b8cb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/stories.tsx
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { strings } from "./i18n/strings.js";
+
+import * as admin from "./paths/admin/index.stories.js";
+import * as instance from "./paths/instance/index.stories.js";
+import * as components from "./components/index.stories.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+import "./scss/main.scss";
+
+function SortStories(a: any, b: any): number {
+ return (a?.order ?? 0) - (b?.order ?? 0);
+}
+
+function main(): void {
+ renderStories(
+ { admin, instance, components },
+ {
+ strings,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/auditor-backoffice-ui/src/sw.js b/packages/auditor-backoffice-ui/src/sw.js
new file mode 100644
index 000000000..bf52db6fa
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/sw.js
@@ -0,0 +1,25 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+// import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
+
+// setupRouting();
+// setupPrecaching(getFiles());
diff --git a/packages/auditor-backoffice-ui/src/utils/amount.ts b/packages/auditor-backoffice-ui/src/utils/amount.ts
new file mode 100644
index 000000000..475489d3e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/amount.ts
@@ -0,0 +1,71 @@
+/*
+ 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/>
+ */
+import {
+ amountFractionalBase,
+ AmountJson,
+ Amounts,
+} from "@gnu-taler/taler-util";
+import { MerchantBackend } from "../declaration.js";
+
+/**
+ * merge refund with the same description and a difference less than one minute
+ * @param prev list of refunds that will hold the merged refunds
+ * @param cur new refund to add to the list
+ * @returns list with the new refund, may be merged with the last
+ */
+export function mergeRefunds(
+ prev: MerchantBackend.Orders.RefundDetails[],
+ cur: MerchantBackend.Orders.RefundDetails,
+): MerchantBackend.Orders.RefundDetails[] {
+ let tail;
+
+ if (
+ prev.length === 0 || //empty list
+ cur.timestamp.t_s === "never" || //current does not have timestamp
+ (tail = prev[prev.length - 1]).timestamp.t_s === "never" || // last does not have timestamp
+ cur.reason !== tail.reason || //different reason
+ cur.pending !== tail.pending || //different pending state
+ Math.abs(cur.timestamp.t_s - tail.timestamp.t_s) > 1000 * 60
+ ) {
+ //more than 1 minute difference
+
+ //can't merge refunds, they are different or to distant in time
+ prev.push(cur);
+ return prev;
+ }
+
+ const a = Amounts.parseOrThrow(tail.amount);
+ const b = Amounts.parseOrThrow(cur.amount);
+ const r = Amounts.add(a, b).amount;
+
+ prev[prev.length - 1] = {
+ ...tail,
+ amount: Amounts.stringify(r),
+ };
+
+ return prev;
+}
+
+export function rate(a: AmountJson, b: AmountJson): number {
+ const af = toFloat(a);
+ const bf = toFloat(b);
+ if (bf === 0) return 0;
+ return af / bf;
+}
+
+function toFloat(amount: AmountJson): number {
+ return amount.value + amount.fraction / amountFractionalBase;
+}
diff --git a/packages/auditor-backoffice-ui/src/utils/constants.ts b/packages/auditor-backoffice-ui/src/utils/constants.ts
new file mode 100644
index 000000000..7c4e288b3
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/constants.ts
@@ -0,0 +1,197 @@
+/*
+ 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)
+ */
+
+//https://tools.ietf.org/html/rfc8905
+export const PAYTO_REGEX =
+ /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/;
+export const PAYTO_WIRE_METHOD_LOOKUP =
+ /payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/;
+
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/;
+
+export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
+
+export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/;
+
+export const CROCKFORD_BASE32_REGEX =
+ /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+[*~$=U]*$/;
+
+export const URL_REGEX =
+ /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/;
+
+// how much rows we add every time user hit load more
+export const PAGE_SIZE = 20;
+// how bigger can be the result set
+// after this threshold, load more with move the cursor
+export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+
+// how much we will wait for all request, in seconds
+export const DEFAULT_REQUEST_TIMEOUT = 10;
+
+export const MAX_IMAGE_SIZE = 1024 * 1024;
+
+export const INSTANCE_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.@-]+$/;
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe",
+};
diff --git a/packages/auditor-backoffice-ui/src/utils/crypto.ts b/packages/auditor-backoffice-ui/src/utils/crypto.ts
new file mode 100644
index 000000000..27e6ade02
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/crypto.ts
@@ -0,0 +1,61 @@
+/*
+ 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 isBase32RFC3548Charset(s: string): boolean {
+ for (let idx = 0; idx < s.length; idx++) {
+ const c = s.charAt(idx);
+ if (encTable.indexOf(c) === -1) return false;
+ }
+ return true;
+}
+
+export function randomBase32Key(): string {
+ var buf = new Uint8Array(20);
+ window.crypto.getRandomValues(buf);
+ return encodeBase32(buf);
+}
diff --git a/packages/auditor-backoffice-ui/src/utils/regex.test.ts b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
new file mode 100644
index 000000000..984f1a472
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
@@ -0,0 +1,88 @@
+/*
+ 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)
+ */
+
+import { expect } from "chai";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "./constants.js";
+
+describe("payto uri format", () => {
+ const valids = [
+ "payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello",
+ "payto://ach/122000661/1234",
+ "payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200",
+ "payto://void/?amount=EUR:10.5",
+ "payto://ilp/g.acme.bob",
+ ];
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(PAYTO_REGEX));
+ });
+
+ const invalids = [
+ // has two question marks
+ "payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello",
+ // has a space
+ "payto://ach /122000661/1234",
+ // has a space
+ "payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200",
+ // invalid field name (mount instead of amount)
+ "payto://void/?mount=EUR:10.5",
+ // payto:// is incomplete
+ "payto: //ilp/g.acme.bob",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(PAYTO_REGEX));
+ });
+});
+
+describe("amount format", () => {
+ const valids = [
+ "ARS:10",
+ "COL:10.2",
+ "UY:1,000.2",
+ "ARS:10.123,123",
+ "ARS:1,000,000",
+ "ARSCOL:10",
+ "LONGESTCURR:1,000,000.123,123",
+ ];
+
+
+ it("should be valid", () => {
+ valids.forEach((v) => expect(v).match(AMOUNT_REGEX));
+ });
+
+ const invalids = [
+ //no currency name
+ ":10",
+ //use . instead of ,
+ "ARS:1.000.000",
+ //currency name with numbers
+ "1ARS:10",
+ //currency name with numbers
+ "AR5:10",
+ //missing value
+ "USD:",
+ ];
+
+ it("should not be valid", () => {
+ invalids.forEach((v) => expect(v).not.match(AMOUNT_REGEX));
+ });
+});
diff --git a/packages/auditor-backoffice-ui/src/utils/table.ts b/packages/auditor-backoffice-ui/src/utils/table.ts
new file mode 100644
index 000000000..db2b2021c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/table.ts
@@ -0,0 +1,57 @@
+/*
+ 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/>
+ */
+
+import { WithId } from "../declaration.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export interface Actions<T extends WithId> {
+ element: T;
+ type: "DELETE" | "UPDATE";
+}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
+
+export function buildActions<T extends WithId>(
+ instances: T[],
+ selected: string[],
+ action: "DELETE",
+): Actions<T>[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
+
+/**
+ * For any object or array, return the same object if is not empty.
+ * not empty:
+ * - for arrays: at least one element not undefined
+ * - for objects: at least one property not undefined
+ * @param obj
+ * @returns
+ */
+export function undefinedIfEmpty<
+ T extends Record<string, unknown> | Array<unknown>,
+>(obj: T | undefined): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.values(obj).some((v) => v !== undefined) ? obj : undefined;
+}
diff --git a/packages/auditor-backoffice-ui/src/utils/types.ts b/packages/auditor-backoffice-ui/src/utils/types.ts
new file mode 100644
index 000000000..0d249f3c4
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/utils/types.ts
@@ -0,0 +1,31 @@
+/*
+ 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/>
+ */
+
+import { VNode } from "preact";
+
+export interface KeyValue {
+ [key: string]: string;
+}
+
+export interface Notification {
+ message: string;
+ description?: string | VNode;
+ details?: string | VNode;
+ type: MessageType;
+}
+
+export type ValueOrFunction<T> = T | ((p: T) => T);
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/auditor-backoffice-ui/test.mjs b/packages/auditor-backoffice-ui/test.mjs
new file mode 100755
index 000000000..be76348e5
--- /dev/null
+++ b/packages/auditor-backoffice-ui/test.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/*
+ 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/>
+ */
+
+import { build } from "@gnu-taler/web-util/build";
+import { getFilesInDirectory } from "@gnu-taler/web-util/build";
+
+const allTestFiles = getFilesInDirectory("src", /.test.tsx?$/);
+
+await build({
+ type: "test",
+ source: {
+ js: allTestFiles.files,
+ assets: [{base:"src",files:["src/index.html"]}],
+ },
+ destination: "./dist/test",
+ css: "sass",
+});
diff --git a/packages/auditor-backoffice-ui/tsconfig.json b/packages/auditor-backoffice-ui/tsconfig.json
new file mode 100644
index 000000000..396f1e9e7
--- /dev/null
+++ b/packages/auditor-backoffice-ui/tsconfig.json
@@ -0,0 +1,58 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
+ "module": "Node16" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+ "lib": [
+ "es2020",
+ "dom"
+ ] /* Specify library files to be included in the compilation: */,
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h" /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */,
+ "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
+ // "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ // "outDir": "./", /* Redirect output structure to the directory. */
+ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "removeComments": true, /* Do not emit comments to output. */
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*", "tests/**/*"]
+}