aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/aml-backoffice-ui/package.json3
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx32
-rw-r--r--packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx34
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx65
-rw-r--r--packages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx (renamed from packages/taler-util/src/libeufin-api-types.ts)23
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts9
-rw-r--r--packages/aml-backoffice-ui/src/hooks/account.ts (renamed from packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts)38
-rw-r--r--packages/aml-backoffice-ui/src/hooks/decisions.ts (renamed from packages/aml-backoffice-ui/src/hooks/useCases.ts)121
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts19
-rw-r--r--packages/aml-backoffice-ui/src/hooks/preferences.ts7
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx853
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx44
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx22
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx334
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx12
-rw-r--r--packages/aml-backoffice-ui/src/pages/Search.tsx724
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx68
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx161
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx10
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-core/src/anastasis-data.ts12
-rw-r--r--packages/anastasis-webui/package.json3
-rw-r--r--packages/auditor-backoffice-ui/README.md2
-rwxr-xr-xpackages/auditor-backoffice-ui/dev.mjs2
-rw-r--r--packages/auditor-backoffice-ui/package.json5
-rw-r--r--packages/auditor-backoffice-ui/src/AdminRoutes.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/Application.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx145
-rw-r--r--packages/auditor-backoffice-ui/src/InstanceRoutes.tsx901
-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.tsx2
-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/forms/FormProvider.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx)18
-rw-r--r--packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx10
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx267
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/index.tsx193
-rw-r--r--packages/auditor-backoffice-ui/src/components/modal/index.tsx410
-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.ts48
-rw-r--r--packages/auditor-backoffice-ui/src/context/config.ts11
-rw-r--r--packages/auditor-backoffice-ui/src/context/entity.ts (renamed from packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx)34
-rw-r--r--packages/auditor-backoffice-ui/src/custom.d.ts2
-rw-r--r--packages/auditor-backoffice-ui/src/declaration.d.ts1755
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/async.ts77
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/backend.ts448
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/bank.ts217
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/critical.ts67
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts161
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/entity.ts83
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/finance.ts67
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/index.ts115
-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/operational.ts80
-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/webhooks.ts178
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/poheader2
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings-prelude2
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/strings.ts2
-rw-r--r--packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot2
-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/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/default/Table.tsx148
-rw-r--r--packages/auditor-backoffice-ui/src/paths/default/index.tsx122
-rw-r--r--packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx383
-rw-r--r--packages/auditor-backoffice-ui/src/paths/details/index.tsx38
-rw-r--r--packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx272
-rw-r--r--packages/auditor-backoffice-ui/src/paths/finance/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx)45
-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/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/CreatedSuccessfully.tsx69
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx46
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx126
-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/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/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/CreatePage.tsx179
-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/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/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx186
-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/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx47
-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/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.tsx94
-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.tsx270
-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/update/UpdatePage.tsx269
-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/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/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.tsx244
-rw-r--r--packages/auditor-backoffice-ui/src/paths/notfound/index.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx93
-rw-r--r--packages/auditor-backoffice-ui/src/paths/operations/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx)56
-rw-r--r--packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx85
-rw-r--r--packages/auditor-backoffice-ui/src/paths/security/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx)53
-rw-r--r--packages/auditor-backoffice-ui/src/paths/settings/index.tsx181
-rw-r--r--packages/auditor-backoffice-ui/src/schemas/index.ts245
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_aside.scss9
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_card.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_footer.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_form.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_hero-bar.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_loading.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_main-section.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_misc.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_modal.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_nav-bar.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_table.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_theme-default.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_tiles.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_title-bar.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/fonts/nunito.css2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/libs/_all.scss2
-rw-r--r--packages/auditor-backoffice-ui/src/scss/main.scss2
-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/utils/amount.ts71
-rw-r--r--packages/auditor-backoffice-ui/src/utils/constants.ts197
-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.ts9
-rw-r--r--packages/bank-ui/package.json4
-rw-r--r--packages/bank-ui/src/Routing.tsx35
-rw-r--r--packages/bank-ui/src/components/Cashouts/views.tsx3
-rw-r--r--packages/bank-ui/src/components/Transactions/views.tsx2
-rw-r--r--packages/bank-ui/src/hooks/account.ts6
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts26
-rw-r--r--packages/bank-ui/src/hooks/regional.ts32
-rw-r--r--packages/bank-ui/src/i18n/de.po154
-rw-r--r--packages/bank-ui/src/i18n/es.po8
-rw-r--r--packages/bank-ui/src/i18n/uk.po672
-rw-r--r--packages/bank-ui/src/pages/AccountPage/index.ts7
-rw-r--r--packages/bank-ui/src/pages/AccountPage/views.tsx4
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx8
-rw-r--r--packages/bank-ui/src/pages/OperationState/index.ts9
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts30
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx305
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.tsx14
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx64
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx15
-rw-r--r--packages/bank-ui/src/pages/ShowNotifications.tsx3
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx44
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx159
-rw-r--r--packages/bank-ui/src/pages/WireTransfer.tsx7
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx55
-rw-r--r--packages/bank-ui/src/pages/WithdrawalOperationPage.tsx4
-rw-r--r--packages/bank-ui/src/pages/WithdrawalQRCode.tsx21
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx231
-rw-r--r--packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx8
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx15
-rw-r--r--packages/bank-ui/src/pages/admin/AccountList.tsx54
-rw-r--r--packages/bank-ui/src/pages/admin/CreateNewAccount.tsx22
-rw-r--r--packages/bank-ui/src/pages/admin/DownloadStats.tsx4
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx15
-rw-r--r--packages/bank-ui/src/pages/regional/ConversionConfig.tsx44
-rw-r--r--packages/bank-ui/src/pages/regional/CreateCashout.tsx56
-rw-r--r--packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx4
-rw-r--r--packages/bank-ui/src/settings.json2
-rw-r--r--packages/bank-ui/src/settings.ts7
-rw-r--r--packages/bank-ui/src/stories.test.ts4
-rw-r--r--packages/bank-ui/src/type-override.d.ts (renamed from packages/auditor-backoffice-ui/src/sw.js)21
-rw-r--r--packages/bank-ui/src/utils.ts2
-rwxr-xr-xpackages/challenger-ui/build.mjs10
-rwxr-xr-xpackages/challenger-ui/create_must.sh25
-rwxr-xr-xpackages/challenger-ui/dev.mjs10
-rw-r--r--packages/challenger-ui/package.json6
-rw-r--r--packages/challenger-ui/src/Routing.tsx264
-rw-r--r--packages/challenger-ui/src/app.tsx14
-rw-r--r--packages/challenger-ui/src/attempts-exhausted.html88
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx52
-rw-r--r--packages/challenger-ui/src/context/preferences.ts87
-rw-r--r--packages/challenger-ui/src/declaration.d.ts35
-rw-r--r--packages/challenger-ui/src/enter-address-form.html133
-rw-r--r--packages/challenger-ui/src/enter-email-form.html127
-rw-r--r--packages/challenger-ui/src/enter-file-access-form.html102
-rw-r--r--packages/challenger-ui/src/enter-phone-form.html126
-rw-r--r--packages/challenger-ui/src/enter-tan-form.html117
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts23
-rw-r--r--packages/challenger-ui/src/hooks/session.ts113
-rw-r--r--packages/challenger-ui/src/i18n/challenger-ui.pot2
-rw-r--r--packages/challenger-ui/src/internal-error.html89
-rw-r--r--packages/challenger-ui/src/invalid-pin.html87
-rw-r--r--packages/challenger-ui/src/invalid-request.html88
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx314
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx463
-rw-r--r--packages/challenger-ui/src/pages/CallengeCompleted.tsx36
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx153
-rw-r--r--packages/challenger-ui/src/pages/Setup.tsx161
-rw-r--r--packages/challenger-ui/src/validation-unknown.html89
-rw-r--r--packages/idb-bridge/package.json2
-rw-r--r--packages/idb-bridge/src/backends.test.ts51
-rw-r--r--packages/idb-bridge/src/bridge-idb.ts12
-rw-r--r--packages/idb-bridge/src/util/errors.ts2
-rw-r--r--packages/kyc-ui/.gitignore4
-rw-r--r--packages/kyc-ui/Makefile36
-rw-r--r--packages/kyc-ui/README.md4
-rwxr-xr-x[-rw-r--r--]packages/kyc-ui/build.mjs (renamed from packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx)29
-rw-r--r--packages/kyc-ui/copyleft-header.js (renamed from packages/auditor-backoffice-ui/src/components/index.stories.ts)4
-rwxr-xr-x[-rw-r--r--]packages/kyc-ui/dev.mjs (renamed from packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx)42
-rw-r--r--packages/kyc-ui/package.json63
-rw-r--r--packages/kyc-ui/postcss.config.js (renamed from packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts)11
-rw-r--r--packages/kyc-ui/src/Routing.tsx127
-rw-r--r--packages/kyc-ui/src/app.tsx169
-rw-r--r--packages/kyc-ui/src/assets/home.svg3
-rw-r--r--packages/kyc-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/kyc-ui/src/assets/people.svg3
-rw-r--r--packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx24
-rw-r--r--packages/kyc-ui/src/context/preferences.ts81
-rw-r--r--packages/kyc-ui/src/context/settings.ts (renamed from packages/auditor-backoffice-ui/src/context/instance.ts)40
-rw-r--r--packages/kyc-ui/src/declaration.d.ts35
-rw-r--r--packages/kyc-ui/src/forms/index.ts45
-rw-r--r--packages/kyc-ui/src/forms/nameAndBirthdate.ts44
-rw-r--r--packages/kyc-ui/src/forms/personal-info.ts66
-rw-r--r--packages/kyc-ui/src/forms/simplest.ts76
-rw-r--r--packages/kyc-ui/src/hooks/form.ts227
-rw-r--r--packages/kyc-ui/src/hooks/kyc.ts55
-rw-r--r--packages/kyc-ui/src/hooks/session.ts60
-rw-r--r--packages/kyc-ui/src/i18n/challenger-ui.pot199
-rw-r--r--packages/kyc-ui/src/i18n/poheader26
-rw-r--r--packages/kyc-ui/src/i18n/strings.ts90
-rw-r--r--packages/kyc-ui/src/index.html41
-rw-r--r--packages/kyc-ui/src/index.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx)17
-rw-r--r--packages/kyc-ui/src/pages/CallengeCompleted.tsx29
-rw-r--r--packages/kyc-ui/src/pages/FillForm.tsx276
-rw-r--r--packages/kyc-ui/src/pages/Frame.tsx132
-rw-r--r--packages/kyc-ui/src/pages/MissingParams.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts)11
-rw-r--r--packages/kyc-ui/src/pages/NonceNotFound.tsx42
-rw-r--r--packages/kyc-ui/src/pages/Start.tsx404
-rw-r--r--packages/kyc-ui/src/scss/main.css3
-rw-r--r--packages/kyc-ui/src/settings.json3
-rw-r--r--packages/kyc-ui/src/settings.ts83
-rw-r--r--packages/kyc-ui/tailwind.config.js (renamed from packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx)24
-rw-r--r--packages/kyc-ui/tsconfig.json46
-rw-r--r--packages/merchant-backend-ui/package.json3
-rwxr-xr-xpackages/merchant-backoffice-ui/dev.mjs2
-rw-r--r--packages/merchant-backoffice-ui/error.db22
-rw-r--r--packages/merchant-backoffice-ui/package.json31
-rw-r--r--packages/merchant-backoffice-ui/preact.config.js70
-rw-r--r--packages/merchant-backoffice-ui/preact.single-config.js62
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx75
-rw-r--r--packages/merchant-backoffice-ui/src/Routing.tsx283
-rw-r--r--packages/merchant-backoffice-ui/src/components/Amount.tsx107
-rw-r--r--packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx243
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/QR.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputArray.tsx92
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx123
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputImage.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx86
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx50
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx100
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx42
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx495
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx56
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx151
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductList.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx138
-rw-r--r--packages/merchant-backoffice-ui/src/context/session.ts25
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts74
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/async.ts10
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/bank.ts31
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/category.ts78
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts44
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.ts14
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/otp.ts14
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/preference.ts6
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.ts44
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts17
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts90
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.ts7
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/urls.ts31
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/webhooks.ts14
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/de.po2939
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/en.po2825
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/es.po4156
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/fr.po2831
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/it.po2843
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/strings.ts3585
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/sv.po2825
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot2807
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/tr.po2724
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/uk.po4124
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx254
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx37
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx83
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx230
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx122
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx61
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx475
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx112
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx332
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx127
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/create/Create.stories.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx)2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx)65
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx)47
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx)64
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx113
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/update/Update.stories.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx)2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx162
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx114
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx83
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx90
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx87
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx93
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx71
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx110
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx79
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx55
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx37
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx65
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx37
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx59
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx75
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx51
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx26
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx34
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx94
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx60
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx55
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx63
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx20
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx93
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx23
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx54
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx46
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx)6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx)16
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx)50
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx)132
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx169
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx161
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx123
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx23
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx95
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx29
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx114
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx21
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx26
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/notfound/index.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx29
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts224
-rw-r--r--packages/merchant-backoffice-ui/src/scss/_aside.scss7
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss16
-rw-r--r--packages/merchant-backoffice-ui/src/settings.json3
-rw-r--r--packages/merchant-backoffice-ui/src/type-override.d.ts (renamed from packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx)24
-rw-r--r--packages/merchant-backoffice-ui/src/utils/table.ts4
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/debian/changelog132
-rw-r--r--packages/taler-harness/gdb.txt26
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/bench2.ts10
-rw-r--r--packages/taler-harness/src/env-full.ts6
-rw-r--r--packages/taler-harness/src/harness/harness.ts205
-rw-r--r--packages/taler-harness/src/harness/helpers.ts171
-rw-r--r--packages/taler-harness/src/index.ts176
-rw-r--r--packages/taler-harness/src/integrationtests/test-account-restrictions.ts179
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-bank-api.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-deposit.ts21
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts114
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-purse.ts1
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-fee-regression.ts7
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts266
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts291
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts367
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts297
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts248
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts311
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts292
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts413
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts410
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts356
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts347
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts322
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc.ts139
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bank.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-categories.ts178
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts38
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-multiexchange.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-fault.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-multiple.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-pull-large.ts11
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts26
-rw-r--r--packages/taler-harness/src/integrationtests/test-repurchase.ts164
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts13
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts1
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts67
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dd48.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts160
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts96
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-notifications.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-observability.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts83
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts20
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-external.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts172
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts78
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts40
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts55
-rw-r--r--packages/taler-util/src/ReserveStatus.ts2
-rw-r--r--packages/taler-util/src/ReserveTransaction.ts2
-rw-r--r--packages/taler-util/src/account-restrictions.ts43
-rw-r--r--packages/taler-util/src/amounts.test.ts2
-rw-r--r--packages/taler-util/src/amounts.ts2
-rw-r--r--packages/taler-util/src/bank-api-client.ts107
-rw-r--r--packages/taler-util/src/clk.ts2
-rw-r--r--packages/taler-util/src/errors.ts39
-rw-r--r--packages/taler-util/src/http-client/authentication.ts11
-rw-r--r--packages/taler-util/src/http-client/bank-conversion.ts13
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts60
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts20
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts10
-rw-r--r--packages/taler-util/src/http-client/bank-wire.ts14
-rw-r--r--packages/taler-util/src/http-client/challenger.ts71
-rw-r--r--packages/taler-util/src/http-client/exchange.ts921
-rw-r--r--packages/taler-util/src/http-client/merchant.ts297
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts41
-rw-r--r--packages/taler-util/src/http-client/types.ts5517
-rw-r--r--packages/taler-util/src/http-client/utils.ts16
-rw-r--r--packages/taler-util/src/http-common.ts76
-rw-r--r--packages/taler-util/src/http-impl.node.ts11
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts35
-rw-r--r--packages/taler-util/src/index.ts42
-rw-r--r--packages/taler-util/src/notifications.ts19
-rw-r--r--packages/taler-util/src/operation.ts102
-rw-r--r--packages/taler-util/src/payto.test.ts27
-rw-r--r--packages/taler-util/src/payto.ts126
-rw-r--r--packages/taler-util/src/qr.ts166
-rw-r--r--packages/taler-util/src/taler-crypto.ts15
-rw-r--r--packages/taler-util/src/taler-error-codes.ts272
-rw-r--r--packages/taler-util/src/taler-signatures.ts69
-rw-r--r--packages/taler-util/src/taler-types.ts2421
-rw-r--r--packages/taler-util/src/taleruri.test.ts14
-rw-r--r--packages/taler-util/src/taleruri.ts12
-rw-r--r--packages/taler-util/src/transaction-test-data.ts4
-rw-r--r--packages/taler-util/src/twrpc.ts2
-rw-r--r--packages/taler-util/src/type-override.d.ts (renamed from packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx)24
-rw-r--r--packages/taler-util/src/types-taler-bank-conversion.ts200
-rw-r--r--packages/taler-util/src/types-taler-bank-integration.ts193
-rw-r--r--packages/taler-util/src/types-taler-challenger.ts293
-rw-r--r--packages/taler-util/src/types-taler-common.ts559
-rw-r--r--packages/taler-util/src/types-taler-corebank.ts916
-rw-r--r--packages/taler-util/src/types-taler-exchange.ts2926
-rw-r--r--packages/taler-util/src/types-taler-merchant.ts3485
-rw-r--r--packages/taler-util/src/types-taler-revenue.ts95
-rw-r--r--packages/taler-util/src/types-taler-sync.ts (renamed from packages/taler-util/src/backup-types.ts)10
-rw-r--r--packages/taler-util/src/types-taler-wallet-transactions.ts (renamed from packages/taler-util/src/transactions-types.ts)87
-rw-r--r--packages/taler-util/src/types-taler-wallet.ts (renamed from packages/taler-util/src/wallet-types.ts)631
-rw-r--r--packages/taler-util/src/types-taler-wire-gateway.ts278
-rw-r--r--packages/taler-util/src/types.test.ts (renamed from packages/taler-util/src/types-test.ts)12
-rw-r--r--packages/taler-util/src/url.ts2
-rw-r--r--packages/taler-util/src/whatwg-url.ts20
-rw-r--r--packages/taler-wallet-cli/debian/changelog126
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts63
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts4
-rw-r--r--packages/taler-wallet-core/src/balance.ts60
-rw-r--r--packages/taler-wallet-core/src/coinSelection.test.ts185
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts701
-rw-r--r--packages/taler-wallet-core/src/common.ts226
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts67
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts19
-rw-r--r--packages/taler-wallet-core/src/crypto/index.ts (renamed from packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx)17
-rw-r--r--packages/taler-wallet-core/src/db.ts437
-rw-r--r--packages/taler-wallet-core/src/dbless.ts44
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts6
-rw-r--r--packages/taler-wallet-core/src/deposits.ts1106
-rw-r--r--packages/taler-wallet-core/src/dev-experiments.ts13
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts1644
-rw-r--r--packages/taler-wallet-core/src/host-impl.node.ts12
-rw-r--r--packages/taler-wallet-core/src/index.ts1
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.test.ts143
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts150
-rw-r--r--packages/taler-wallet-core/src/observable-wrappers.ts5
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts910
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts78
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts696
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts247
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts795
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts447
-rw-r--r--packages/taler-wallet-core/src/query.ts160
-rw-r--r--packages/taler-wallet-core/src/recoup.ts168
-rw-r--r--packages/taler-wallet-core/src/refresh.ts221
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts168
-rw-r--r--packages/taler-wallet-core/src/testing.ts37
-rw-r--r--packages/taler-wallet-core/src/transactions.ts1745
-rw-r--r--packages/taler-wallet-core/src/versions.ts7
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts210
-rw-r--r--packages/taler-wallet-core/src/wallet.ts2463
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts1093
-rw-r--r--packages/taler-wallet-embedded/package.json2
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/manifest-v2.json4
-rw-r--r--packages/taler-wallet-webextension/package.json2
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx115
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx418
-rw-r--r--packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/components/MultiActionButton.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/components/SelectList.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowBanksForPaytoPopup.tsx61
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowQRsForPaytoPopup.tsx94
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/state.ts78
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx116
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx171
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/index.ts10
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/state.ts70
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/test.ts73
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/views.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts8
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx7
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts6
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts5
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts13
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts93
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx7
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts5
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts42
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts199
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx108
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts6
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx62
-rw-r--r--packages/taler-wallet-webextension/src/cta/termsExample.ts23
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po63
-rw-r--r--packages/taler-wallet-webextension/src/i18n/es.po8
-rw-r--r--packages/taler-wallet-webextension/src/i18n/tr.po232
-rw-r--r--packages/taler-wallet-webextension/src/i18n/uk.po693
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts1
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts79
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts5
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts3
-rw-r--r--packages/taler-wallet-webextension/src/popup/Application.tsx64
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx29
-rw-r--r--packages/taler-wallet-webextension/src/pwa/index.html91
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts149
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts30
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx524
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts55
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts50
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts30
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts178
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx18
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx126
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx80
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx67
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts67
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx60
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx121
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts44
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts69
-rw-r--r--packages/web-util/package.json3
-rw-r--r--packages/web-util/src/components/Time.tsx (renamed from packages/bank-ui/src/components/Time.tsx)2
-rw-r--r--packages/web-util/src/components/index.ts1
-rw-r--r--packages/web-util/src/context/api.ts15
-rw-r--r--packages/web-util/src/context/bank-api.ts18
-rw-r--r--packages/web-util/src/context/challenger-api.ts6
-rw-r--r--packages/web-util/src/context/exchange-api.ts6
-rw-r--r--packages/web-util/src/context/merchant-api.ts18
-rw-r--r--packages/web-util/src/context/navigation.ts3
-rw-r--r--packages/web-util/src/context/translation.ts2
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx41
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx2
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx38
-rw-r--r--packages/web-util/src/forms/InputAmount.stories.tsx37
-rw-r--r--packages/web-util/src/forms/InputArray.stories.tsx60
-rw-r--r--packages/web-util/src/forms/InputArray.tsx10
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx60
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.stories.tsx60
-rw-r--r--packages/web-util/src/forms/InputFile.stories.tsx46
-rw-r--r--packages/web-util/src/forms/InputInteger.stories.tsx39
-rw-r--r--packages/web-util/src/forms/InputLine.stories.tsx36
-rw-r--r--packages/web-util/src/forms/InputLine.tsx24
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.stories.tsx99
-rw-r--r--packages/web-util/src/forms/InputSelectOne.stories.tsx62
-rw-r--r--packages/web-util/src/forms/InputText.stories.tsx36
-rw-r--r--packages/web-util/src/forms/InputTextArea.stories.tsx36
-rw-r--r--packages/web-util/src/forms/InputToggle.stories.tsx36
-rw-r--r--packages/web-util/src/forms/forms.ts58
-rw-r--r--packages/web-util/src/forms/ui-form.ts11
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts2
-rw-r--r--packages/web-util/src/index.browser.ts1
-rw-r--r--packages/web-util/src/index.build.ts1
-rw-r--r--packages/web-util/src/live-reload.ts22
-rw-r--r--packages/web-util/src/utils/base64.ts75
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts137
-rw-r--r--packages/web-util/src/utils/request.ts20
-rw-r--r--packages/web-util/src/utils/route.ts33
800 files changed, 69542 insertions, 61172 deletions
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
index 9c33862f7..4394ebb1d 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,13 +1,12 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.13.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
"type": "module",
"scripts": {
- "build": "./build.mjs",
"typedoc": "typedoc --out dist/typedoc ./src/",
"check": "tsc",
"clean": "rm -rf dist lib",
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index e9be84441..c5a935044 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -13,7 +13,12 @@
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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import {
+ CacheEvictor,
+ TalerExchangeCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+} from "@gnu-taler/taler-util";
import {
BrowserHashNavigationProvider,
ExchangeApiProvider,
@@ -31,6 +36,8 @@ import { strings } from "./i18n/strings.js";
import "./scss/main.css";
import { UiSettings, fetchUiSettings } from "./context/ui-settings.js";
import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js";
+import { revalidateAccountDecisions } from "./hooks/decisions.js";
+import { revalidateAccountInformation } from "./hooks/account.js";
const WITH_LOCAL_STORAGE_CACHE = false;
@@ -56,6 +63,9 @@ export function App(): VNode {
<ExchangeApiProvider
baseUrl={new URL("/", baseUrl)}
frameOnError={ExchangeAmlFrame}
+ evictors={{
+ exchange: evictExchangeSwrCache,
+ }}
>
<SWRConfig
value={{
@@ -111,7 +121,7 @@ function getInitialBackendBaseURL(
): string {
const overrideUrl =
typeof localStorage !== "undefined"
- ? localStorage.getItem("exchange-base-url")
+ ? localStorage.getItem("aml-base-url")
: undefined;
let result: string;
@@ -136,3 +146,21 @@ function getInitialBackendBaseURL(
return canonicalizeBaseUrl(window.origin);
}
}
+
+const evictExchangeSwrCache: CacheEvictor<TalerExchangeCacheEviction> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerExchangeCacheEviction.MAKE_AML_DECISION: {
+ await revalidateAccountDecisions();
+ await revalidateAccountInformation();
+ return;
+ }
+ case TalerExchangeCacheEviction.UPLOAD_KYC_FORM: {
+ return;
+ }
+ default: {
+ assertUnreachable(op);
+ }
+ }
+ },
+};
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
index 772fd1b70..a74cd09b9 100644
--- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -33,7 +33,12 @@ import {
getLabelForPreferences,
usePreferences,
} from "./hooks/preferences.js";
-import { HomeIcon } from "./pages/Cases.js";
+import {
+ HomeIcon,
+ PeopleIcon,
+ SearchIcon,
+ ToInvestigateIcon,
+} from "./pages/Cases.js";
/**
* mapping route to view
@@ -110,7 +115,7 @@ export function ExchangeAmlFrame({
children,
officer,
}: {
- officer?: OfficerState,
+ officer?: OfficerState;
children?: ComponentChildren;
}): VNode {
const { i18n } = useTranslationContext();
@@ -133,7 +138,7 @@ export function ExchangeAmlFrame({
}, [error]);
const [preferences, updatePreferences] = usePreferences();
- const settings = useUiSettingsContext()
+ const settings = useUiSettingsContext();
return (
<div
@@ -208,12 +213,17 @@ export function ExchangeAmlFrame({
<div class="-mt-32 flex grow ">
{officer?.state !== "ready" ? undefined : <Navigation />}
<div class="flex mx-auto my-4">
- <main class="rounded-lg bg-white px-5 py-6 shadow">{children}</main>
+ <main
+ class="block rounded-lg bg-white px-5 py-6 shadow "
+ style={{ minWidth: 600 }}
+ >
+ {children}
+ </main>
</div>
</div>
<Footer
- testingUrlKey="exchange-base-url"
+ testingUrlKey="aml-base-url"
GIT_HASH={GIT_HASH}
VERSION={VERSION}
/>
@@ -224,8 +234,18 @@ export function ExchangeAmlFrame({
function Navigation(): VNode {
const { i18n } = useTranslationContext();
const pageList = [
- { route: privatePages.account, Icon: HomeIcon, label: i18n.str`Account` },
- { route: privatePages.cases, Icon: HomeIcon, label: i18n.str`Cases` },
+ { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile` },
+ {
+ route: privatePages.investigation,
+ Icon: ToInvestigateIcon,
+ label: i18n.str`Investigation`,
+ },
+ { route: privatePages.active, Icon: HomeIcon, label: i18n.str`Active` },
+ {
+ route: privatePages.search,
+ Icon: SearchIcon,
+ label: i18n.str`Search`,
+ },
];
const { path } = useNavigationContext();
return (
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx
index f38fc29c2..d69b47184 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -15,6 +15,7 @@
*/
import {
+ decodeCrockFromURI,
urlPattern,
useCurrentLocation,
useNavigationContext,
@@ -22,15 +23,16 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { assertUnreachable } from "@gnu-taler/taler-util";
+import { assertUnreachable, parsePaytoUri, PaytoString } from "@gnu-taler/taler-util";
import { useEffect } from "preact/hooks";
import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js";
import { useOfficer } from "./hooks/officer.js";
-import { Cases } from "./pages/Cases.js";
+import { Cases, CasesUnderInvestigation } from "./pages/Cases.js";
import { Officer } from "./pages/Officer.js";
import { CaseDetails } from "./pages/CaseDetails.js";
import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js";
import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js";
+import { Search } from "./pages/Search.js";
export function Routing(): VNode {
const session = useOfficer();
@@ -62,15 +64,14 @@ function PublicRounting(): VNode {
// const [notification, notify, handleError] = useLocalNotification();
const session = useOfficer();
- if (location === undefined) {
- if (session.state !== "ready") {
- return <HandleAccountNotReady officer={session}/>;
- } else {
- return <div />
- }
- }
-
switch (location.name) {
+ case undefined: {
+ if (session.state !== "ready") {
+ return <HandleAccountNotReady officer={session} />;
+ } else {
+ return <div />;
+ }
+ }
case "config": {
return (
<Fragment>
@@ -95,8 +96,10 @@ function PublicRounting(): VNode {
}
export const privatePages = {
- account: urlPattern(/\/account/, () => "#/account"),
- cases: urlPattern(/\/cases/, () => "#/cases"),
+ profile: urlPattern(/\/profile/, () => "#/profile"),
+ search: urlPattern(/\/search/, () => "#/search"),
+ investigation: urlPattern(/\/investigation/, () => "#/investigation"),
+ active: urlPattern(/\/active/, () => "#/active"),
caseUpdate: urlPattern<{ cid: string; type: string }>(
/\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/,
({ cid, type }) => `#/case/${cid}/new/${type}`,
@@ -105,6 +108,10 @@ export const privatePages = {
/\/case\/(?<cid>[a-zA-Z0-9]+)\/new/,
({ cid }) => `#/case/${cid}/new`,
),
+ caseDetailsNewAccount: urlPattern<{ cid: string, payto: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)\/(?<payto>[a-zA-Z0-9]+)/,
+ ({ cid, payto }) => `#/case/${cid}/${payto}`,
+ ),
caseDetails: urlPattern<{ cid: string }>(
/\/case\/(?<cid>[a-zA-Z0-9]+)/,
({ cid }) => `#/case/${cid}`,
@@ -115,36 +122,44 @@ function PrivateRouting(): VNode {
const { navigateTo } = useNavigationContext();
const location = useCurrentLocation(privatePages);
useEffect(() => {
- if (location === undefined) {
- navigateTo(privatePages.account.url({}));
+ if (location.name === undefined) {
+ navigateTo(privatePages.profile.url({}));
}
}, [location]);
- if (location === undefined) {
- return <Fragment />;
- }
-
switch (location.name) {
- case "account": {
+ case undefined: {
+ return <Fragment />;
+ }
+ case "profile": {
return <Officer />;
}
+ case "caseUpdate": {
+ return (
+ <CaseUpdate account={location.values.cid} type={location.values.type} />
+ );
+ }
case "caseDetails": {
return <CaseDetails account={location.values.cid} />;
}
- case "caseUpdate": {
+ case "caseDetailsNewAccount": {
+ console.log(location.values)
return (
- <CaseUpdate
- account={location.values.cid}
- type={location.values.type}
- />
+ <CaseDetails account={location.values.cid} paytoString={decodeCrockFromURI(location.values.payto)} />
);
}
case "caseNew": {
return <SelectForm account={location.values.cid} />;
}
- case "cases": {
+ case "investigation": {
+ return <CasesUnderInvestigation />;
+ }
+ case "active": {
return <Cases />;
}
+ case "search": {
+ return <Search />;
+ }
default:
assertUnreachable(location);
}
diff --git a/packages/taler-util/src/libeufin-api-types.ts b/packages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx
index aa3d0cb7a..8679af050 100644
--- a/packages/taler-util/src/libeufin-api-types.ts
+++ b/packages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2023 Taler Systems S.A.
+ (C) 2022-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
@@ -13,19 +13,12 @@
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 { TalerError } from "@gnu-taler/taler-util";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { usePreferences } from "../hooks/preferences.js";
-export type FacadeCredentials =
- | NoFacadeCredentials
- | BasicAuthFacadeCredentials;
-export interface NoFacadeCredentials {
- type: "none";
-}
-export interface BasicAuthFacadeCredentials {
- type: "basic";
-
- // Username to use to authenticate
- username: string;
-
- // Password to use to authenticate
- password: string;
+export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode {
+ const [pref] = usePreferences();
+ return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />;
}
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 4cd781b74..215b0ba51 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -29,8 +29,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
fields: [
{
type: "textArea",
- id: ".comment" as UIHandlerId,
- name: "comment",
+ id: "comment" as UIHandlerId,
label: i18n.str`Comment`,
},
],
@@ -59,8 +58,7 @@ export function resolutionSection(
fields: [
{
type: "choiceHorizontal",
- id: ".state" as UIHandlerId,
- name: "state",
+ id: "state" as UIHandlerId,
label: i18n.str`New state`,
converterId: "TalerExchangeApi.AmlState",
choices: [
@@ -80,9 +78,8 @@ export function resolutionSection(
},
{
type: "amount",
- id: ".threshold" as UIHandlerId,
+ id: "threshold" as UIHandlerId,
currency: "NETZBON",
- name: "threshold",
converterId: "Taler.Amount",
label: i18n.str`New threshold`,
},
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/account.ts
index 78574ada4..e2b590a68 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/account.ts
@@ -13,25 +13,47 @@
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 { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
+import {
+ encodeCrock,
+ hashPaytoUri,
+ OfficerAccount,
+ PaytoString,
+ PaytoUri,
+ stringifyPaytoUri,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook } from "swr";
+import _useSWR, { SWRHook, mutate } from "swr";
import { useOfficer } from "./officer.js";
import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
-export function useCaseDetails(paytoHash: string) {
+export function revalidateAccountInformation() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) &&
+ key[key.length - 1] === "getAmlAttributesForAccount",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useAccountInformation(paytoHash: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const { lib: {exchange: api} } = useExchangeApiContext();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
- return await api.getDecisionDetails(officer, account)
+ return await api.getAmlAttributesForAccount(officer, account);
}
- const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionDetails">, TalerHttpError>(
- !session ? undefined : [session, paytoHash], fetcher, {
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getAmlAttributesForAccount">,
+ TalerHttpError
+ >(!session ? undefined : [session, paytoHash], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -47,5 +69,3 @@ export function useCaseDetails(paytoHash: string) {
if (error) return error;
return undefined;
}
-
-
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts
index d3a1c1018..24941b29e 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/decisions.ts
@@ -19,13 +19,12 @@ import { useState } from "preact/hooks";
import {
OfficerAccount,
OperationOk,
- TalerExchangeApi,
TalerExchangeResultByMethod,
TalerHttpError,
} from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
-import { useOfficer } from "./officer.js";
import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useOfficer } from "./officer.js";
const useSWR = _useSWR as unknown as SWRHook;
export const PAGINATED_LIST_SIZE = 10;
@@ -34,12 +33,111 @@ export const PAGINATED_LIST_SIZE = 10;
export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
/**
- * FIXME: mutate result when balance change (transaction )
* @param account
* @param args
* @returns
*/
-export function useCases(state: TalerExchangeApi.AmlState) {
+export function useCurrentDecisionsUnderInvestigation() {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [offset, setOffset] = useState<string>();
+
+ async function fetcher([officer, offset, investigation]: [
+ OfficerAccount,
+ string | undefined,
+ boolean | undefined,
+ ]) {
+ return await api.getAmlDecisions(officer, {
+ order: "dec",
+ offset,
+ investigation: true,
+ active: true,
+ limit: PAGINATED_LIST_REQUEST,
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getAmlDecisions">,
+ TalerHttpError
+ >(
+ !session
+ ? undefined
+ : [session, offset, "getAmlDecisions"],
+ fetcher,
+ );
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
+
+/**
+ * @param account
+ * @param args
+ * @returns
+ */
+export function useCurrentDecisions() {
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [offset, setOffset] = useState<string>();
+
+ async function fetcher([officer, offset]: [
+ OfficerAccount,
+ string | undefined,
+ boolean | undefined,
+ ]) {
+ return await api.getAmlDecisions(officer, {
+ order: "dec",
+ offset,
+ active: true,
+ limit: PAGINATED_LIST_REQUEST,
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getAmlDecisions">,
+ TalerHttpError
+ >(
+ !session
+ ? undefined
+ : [session, offset, "getAmlDecisions"],
+ fetcher,
+ );
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
+
+export function revalidateAccountDecisions() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAmlDecisions",
+ undefined,
+ { revalidate: true },
+ );
+}
+/**
+ * @param account
+ * @param args
+ * @returns
+ */
+export function useAccountDecisions(accountStr: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
const {
@@ -48,23 +146,24 @@ export function useCases(state: TalerExchangeApi.AmlState) {
const [offset, setOffset] = useState<string>();
- async function fetcher([officer, state, offset]: [
+ async function fetcher([officer, account, offset]: [
OfficerAccount,
- TalerExchangeApi.AmlState,
+ string,
string | undefined,
]) {
- return await api.getDecisionsByState(officer, state, {
- order: "asc",
+ return await api.getAmlDecisions(officer, {
+ order: "dec",
offset,
+ account,
limit: PAGINATED_LIST_REQUEST,
});
}
const { data, error } = useSWR<
- TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerExchangeResultByMethod<"getAmlDecisions">,
TalerHttpError
>(
- !session ? undefined : [session, state, offset, "getDecisionsByState"],
+ !session ? undefined : [session, accountStr, offset, "getAmlDecisions"],
fetcher,
);
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index 70b2db571..375dbb190 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -126,14 +126,14 @@ export function useFormState<T>(
shape: Array<UIHandlerId>,
defaultValue: RecursivePartial<FormValues<T>>,
check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
-): [FormHandler<T>, FormStatus<T>] {
+): { handler: FormHandler<T>; status: FormStatus<T> } {
const [form, updateForm] =
useState<RecursivePartial<FormValues<T>>>(defaultValue);
const status = check(form);
const handler = constructFormHandler(shape, form, updateForm, status.errors);
- return [handler, status];
+ return { handler, status };
}
interface Tree<T> extends Record<string, Tree<T> | T> {}
@@ -163,7 +163,10 @@ export function setValueDeeper(object: any, names: string[], value: any): any {
if (object === undefined) {
return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
}
- return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+ return undefinedIfEmpty({
+ ...object,
+ [head]: setValueDeeper(object[head] ?? {}, rest, value),
+ });
}
export function getShapeFromFields(
@@ -179,10 +182,7 @@ export function getShapeFromFields(
}
shape.push(field.id);
} else if (field.type === "group") {
- Array.prototype.push.apply(
- shape,
- getShapeFromFields(field.fields),
- );
+ Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
}
});
return shape;
@@ -204,10 +204,7 @@ export function getRequiredFields(
}
shape.push(field.id);
} else if (field.type === "group") {
- Array.prototype.push.apply(
- shape,
- getRequiredFields(field.fields),
- );
+ Array.prototype.push.apply(shape, getRequiredFields(field.fields));
}
});
return shape;
diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
index 12e85d249..d329cdbb2 100644
--- a/packages/aml-backoffice-ui/src/hooks/preferences.ts
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -27,6 +27,7 @@ import {
} from "@gnu-taler/web-util/browser";
interface Preferences {
+ showDebugInfo: boolean;
allowInsecurePassword: boolean;
keepSessionAfterReload: boolean;
}
@@ -34,16 +35,18 @@ interface Preferences {
export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
.property("allowInsecurePassword", (codecForBoolean()))
+ .property("showDebugInfo", codecForBoolean())
.property("keepSessionAfterReload", (codecForBoolean()))
.build("Preferences");
const defaultPreferences: Preferences = {
allowInsecurePassword: false,
+ showDebugInfo: false,
keepSessionAfterReload: false,
};
const PREFERENCES_KEY = buildStorageKey(
- "exchange-preferences",
+ "aml-preferences",
codecForPreferences(),
);
/**
@@ -69,6 +72,7 @@ export function usePreferences(): [
export function getAllBooleanPreferences(): Array<keyof Preferences> {
return [
+ "showDebugInfo",
"allowInsecurePassword",
"keepSessionAfterReload",
];
@@ -79,6 +83,7 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
+ case "showDebugInfo": return i18n.str`Show debug info`
case "allowInsecurePassword": return i18n.str`Allow Insecure password`
case "keepSessionAfterReload": return i18n.str`Keep session after reload`
}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index bb936cebf..88366c1d0 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -18,9 +18,11 @@ import {
AmountJson,
Amounts,
Codec,
+ CurrencySpecification,
HttpStatusCode,
OperationFail,
OperationOk,
+ PaytoString,
TalerError,
TalerErrorDetail,
TalerExchangeApi,
@@ -32,21 +34,26 @@ import {
codecOptional,
} from "@gnu-taler/taler-util";
import {
+ Attention,
DefaultForm,
- ErrorLoading,
FormMetadata,
InternationalizationAPI,
Loading,
+ ShowInputErrorLabel,
+ Time,
+ useExchangeApiContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { VNode, h } from "preact";
+import { format, formatDuration, intervalToDuration } from "date-fns";
+import { Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { privatePages } from "../Routing.js";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useUiFormsContext } from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
-import { useCaseDetails } from "../hooks/useCaseDetails.js";
+import { useAccountInformation } from "../hooks/account.js";
+import { useAccountDecisions } from "../hooks/decisions.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
+import { useOfficer } from "../hooks/officer.js";
export type AmlEvent =
| AmlFormEvent
@@ -77,7 +84,7 @@ type KycCollectionEvent = {
when: AbsoluteTime;
title: TranslatedString;
values: object;
- provider: string;
+ provider?: string;
};
type KycExpirationEvent = {
type: "kyc-expiration";
@@ -115,68 +122,82 @@ function titleForJustification(
}
export function getEventsFromAmlHistory(
- aml: TalerExchangeApi.AmlDecisionDetail[],
- kyc: TalerExchangeApi.KycDetail[],
+ events: TalerExchangeApi.KycAttributeCollectionEvent[],
i18n: InternationalizationAPI,
forms: FormMetadata[],
): AmlEvent[] {
- const ae: AmlEvent[] = aml.map((a) => {
- const just = parseJustification(a.justification, forms);
+ // const ae: AmlEvent[] = aml.map((a) => {
+ // const just = parseJustification(a.justification, forms);
+ // return {
+ // type: just.type === "ok" ? "aml-form" : "aml-form-error",
+ // state: a.new_state,
+ // threshold: Amounts.parseOrThrow(a.new_threshold),
+ // title: titleForJustification(just, i18n),
+ // metadata: just.type === "ok" ? just.body.metadata : undefined,
+ // justification: just.type === "ok" ? just.body.justification : undefined,
+ // when: {
+ // t_ms:
+ // a.decision_time.t_s === "never"
+ // ? "never"
+ // : a.decision_time.t_s * 1000,
+ // },
+ // } as AmlEvent;
+ // });
+ // const ke = kyc.reduce((prev, k) => {
+ // prev.push({
+ // type: "kyc-collection",
+ // title: i18n.str`collection`,
+ // when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
+ // values: !k.attributes ? {} : k.attributes,
+ // provider: k.provider_section,
+ // });
+ // prev.push({
+ // type: "kyc-expiration",
+ // title: i18n.str`expiration`,
+ // when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
+ // fields: !k.attributes ? [] : Object.keys(k.attributes),
+ // });
+ // return prev;
+ // }, [] as AmlEvent[]);
+
+ const ke = events.map((event) => {
return {
- type: just.type === "ok" ? "aml-form" : "aml-form-error",
- state: a.new_state,
- threshold: Amounts.parseOrThrow(a.new_threshold),
- title: titleForJustification(just, i18n),
- metadata: just.type === "ok" ? just.body.metadata : undefined,
- justification: just.type === "ok" ? just.body.justification : undefined,
- when: {
- t_ms:
- a.decision_time.t_s === "never"
- ? "never"
- : a.decision_time.t_s * 1000,
- },
- } as AmlEvent;
- });
- const ke = kyc.reduce((prev, k) => {
- prev.push({
type: "kyc-collection",
title: i18n.str`collection`,
- when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
- values: !k.attributes ? {} : k.attributes,
- provider: k.provider_section,
- });
- prev.push({
- type: "kyc-expiration",
- title: i18n.str`expiration`,
- when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
- fields: !k.attributes ? [] : Object.keys(k.attributes),
- });
- return prev;
- }, [] as AmlEvent[]);
- return ae.concat(ke).sort(selectSooner);
+ when: AbsoluteTime.fromProtocolTimestamp(event.collection_time),
+ values: !event.attributes ? {} : event.attributes,
+ provider: event.provider_name,
+ } as AmlEvent;
+ });
+ return ke.sort(selectSooner);
}
-export function CaseDetails({ account }: { account: string }) {
+export function CaseDetails({ account, paytoString }: { account: string, paytoString?: PaytoString }) {
const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
const [showForm, setShowForm] = useState<{
justification: Justification;
metadata: FormMetadata;
}>();
+ const { config, lib } = useExchangeApiContext();
+ const officer = useOfficer();
+ const session = officer.state === "ready" ? officer.account : undefined;
const { i18n } = useTranslationContext();
- const details = useCaseDetails(account);
+ const details = useAccountInformation(account);
+ const history = useAccountDecisions(account);
+
const { forms } = useUiFormsContext();
const allForms = [...forms, ...preloadedForms(i18n)];
- if (!details) {
+ if (!details || !history) {
return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoading error={details} />;
+ return <ErrorLoadingWithDebug error={details} />;
}
if (details.type === "fail") {
switch (details.case) {
- case HttpStatusCode.Unauthorized:
+ // case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
case HttpStatusCode.Conflict:
@@ -185,44 +206,51 @@ export function CaseDetails({ account }: { account: string }) {
assertUnreachable(details);
}
}
- const { aml_history, kyc_attributes } = details.body;
+ if (history instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={history} />;
+ }
+ if (history.type === "fail") {
+ switch (history.case) {
+ // case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ return <div />;
+ default:
+ assertUnreachable(history);
+ }
+ }
+ const { details: accountDetails } = details.body;
+ const activeDecision = history.body.find((d) => d.is_active);
+ const restDecisions = !activeDecision
+ ? history.body
+ : history.body.filter((d) => d.rowid !== activeDecision.rowid);
- const events = getEventsFromAmlHistory(
- aml_history,
- kyc_attributes,
- i18n,
- allForms,
- );
+ const events = getEventsFromAmlHistory(accountDetails, i18n, allForms);
- if (showForm !== undefined) {
- return (
- <DefaultForm
- readOnly={true}
- initial={showForm.justification.value}
- form={showForm.metadata as any} // FIXME: HERE
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <button
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={() => {
- setShowForm(undefined);
- }}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- </div>
- </DefaultForm>
- );
- }
- return (
- <div>
- <a
- href={privatePages.caseNew.url({ cid: account })}
- class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
- >
- <i18n.Translate>New AML form</i18n.Translate>
- </a>
+ // if (showForm !== undefined) {
+ // return (
+ // <DefaultForm
+ // readOnly={true}
+ // initial={showForm.justification.value}
+ // form={showForm.metadata as any} // FIXME: HERE
+ // >
+ // <div class="mt-6 flex items-center justify-end gap-x-6">
+ // <button
+ // class="text-sm font-semibold leading-6 text-gray-900"
+ // onClick={() => {
+ // setShowForm(undefined);
+ // }}
+ // >
+ // <i18n.Translate>Cancel</i18n.Translate>
+ // </button>
+ // </div>
+ // </DefaultForm>
+ // );
+ // }
+ return (
+ <div class="min-w-60">
<header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
<h1 class="text-base font-semibold leading-7 text-black">
<i18n.Translate>
@@ -231,30 +259,382 @@ export function CaseDetails({ account }: { account: string }) {
</i18n.Translate>
</h1>
</header>
- <ShowTimeline
- history={events}
- onSelect={(e) => {
- switch (e.type) {
- case "aml-form": {
- const { justification, metadata } = e;
- setShowForm({ justification, metadata });
- break;
- }
- case "kyc-collection":
- case "kyc-expiration": {
- setSelected(e.when);
- break;
+
+ {!activeDecision || !activeDecision.to_investigate ? undefined : (
+ <Attention title={i18n.str`Under investigation`} type="warning">
+ <i18n.Translate>
+ This account requires a manual review and is waiting for a decision
+ to be made.
+ </i18n.Translate>
+ </Attention>
+ )}
+
+ <div>
+ <button
+ onClick={async () => {
+ if (!session) return;
+ lib.exchange.makeAmlDesicion(session, {
+ payto_uri: paytoString,
+ decision_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.now(),
+ ),
+ h_payto: account,
+ justification: "",
+ keep_investigating: false,
+ properties: {},
+ new_rules: {
+ custom_measures: {},
+ expiration_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.never(),
+ ),
+ rules: FREEZE_RULES(config.currency),
+ successor_measure: "verboten",
+ },
+ });
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>Freeze account</i18n.Translate>
+ </button>
+ <button
+ onClick={async () => {
+ if (!session) return;
+ lib.exchange.makeAmlDesicion(session, {
+ payto_uri: paytoString,
+ decision_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.now(),
+ ),
+ h_payto: account,
+ justification: "",
+ keep_investigating: false,
+ properties: {},
+ new_rules: {
+ custom_measures: {},
+ expiration_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.never(),
+ ),
+ rules: THRESHOLD_100_HOUR(config.currency),
+ successor_measure: "verboten",
+ },
+ });
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>Set threshold to 100 / hour</i18n.Translate>
+ </button>
+ <button
+ onClick={async () => {
+ if (!session) return;
+ lib.exchange.makeAmlDesicion(session, {
+ payto_uri: paytoString,
+ decision_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.now(),
+ ),
+ h_payto: account,
+ justification: "",
+ keep_investigating: false,
+ properties: {},
+ new_rules: {
+ custom_measures: {},
+ expiration_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.never(),
+ ),
+ rules: THRESHOLD_2000_WEEK(config.currency),
+ successor_measure: "verboten",
+ },
+ });
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>Set threshold to 2000 / week</i18n.Translate>
+ </button>
+ <button
+ onClick={async () => {
+ if (!session) return;
+ lib.exchange.makeAmlDesicion(session, {
+ payto_uri: paytoString,
+ decision_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.now(),
+ ),
+ h_payto: account,
+ justification: "",
+ keep_investigating: false,
+ properties: {},
+ new_measures: "m2",
+ new_rules: {
+ custom_measures: {},
+ expiration_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.never(),
+ ),
+ rules: FREEZE_RULES(config.currency),
+ successor_measure: "verboten",
+ },
+ });
+ }}
+ class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ <i18n.Translate>Ask for more information</i18n.Translate>
+ </button>
+ </div>
+
+ {!activeDecision ? (
+ <Attention title={i18n.str`No active rules found`} type="warning" />
+ ) : (
+ <Fragment>
+ <h1 class="my-4 text-base font-semibold leading-6 text-black">
+ <i18n.Translate>Current active rules</i18n.Translate>
+ </h1>
+ <ShowDecisionInfo decision={activeDecision} startOpen />
+ </Fragment>
+ )}
+ <h1 class="my-4 text-base font-semibold leading-6 text-black">
+ <i18n.Translate>KYC collection events</i18n.Translate>
+ </h1>
+ {events.length === 0 ?
+ <Attention title={i18n.str`No events found`} type="warning" />
+ :
+ <ShowTimeline
+ history={events}
+ onSelect={(e) => {
+ switch (e.type) {
+ case "aml-form": {
+ // const { justification, metadata } = e;
+ // setShowForm({ justification, metadata });
+ break;
+ }
+ case "kyc-collection":
+ case "kyc-expiration": {
+ setSelected(e.when);
+ break;
+ }
+ case "aml-form-error":
}
- case "aml-form-error":
- }
- }}
- />
+ }}
+ />
+ }
{/* {selected && <ShowEventDetails event={selected} />} */}
{selected && <ShowConsolidated history={events} until={selected} />}
+ {restDecisions.length > 0 ? (
+ <Fragment>
+ <h1 class="my-4 text-base font-semibold leading-6 text-black">
+ <i18n.Translate>Previous AML decisions</i18n.Translate>
+ </h1>
+ {restDecisions.map((d) => {
+ return <ShowDecisionInfo decision={d} />;
+ })}
+ </Fragment>
+ ) : (
+ !activeDecision ?
+ <div class="ty-4">
+ <Attention title={i18n.str`No aml history found`} type="warning" />
+ </div> : undefined
+ )}
+ </div>
+ );
+}
+
+function ShowDecisionInfo({
+ decision,
+ startOpen,
+}: {
+ decision: TalerExchangeApi.AmlDecision;
+ startOpen?: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useExchangeApiContext();
+ const [opened, setOpened] = useState(startOpen ?? false);
+
+ function Header() {
+ return (
+ <ul
+ role="list"
+ class="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
+ >
+ <li class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6">
+ <div class="flex min-w-0 gap-x-4">
+ <div class="flex mt-2 rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3">
+ <i18n.Translate>Since</i18n.Translate>
+ </div>
+ <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6">
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ decision.decision_time,
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ <div class="flex shrink-0 items-center gap-x-4">
+ <div class="flex mt-2 rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3">
+ {AbsoluteTime.isExpired(
+ AbsoluteTime.fromProtocolTimestamp(
+ decision.limits.expiration_time,
+ ),
+ ) ? (
+ <i18n.Translate>Expired</i18n.Translate>
+ ) : (
+ <i18n.Translate>Expires</i18n.Translate>
+ )}
+ </div>
+ <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6">
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ decision.limits.expiration_time,
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ );
+ }
+
+ if (!opened) {
+ return (
+ <div class="mt-4 cursor-pointer" onClick={() => setOpened(true)}>
+ <Header />
+ </div>
+ );
+ }
+ const balanceLimit = decision.limits.rules.find(
+ (r) => r.operation_type === "BALANCE",
+ );
+
+ return (
+ <div>
+ <div class="mt-4 cursor-pointer" onClick={() => setOpened(false)}>
+ <Header />
+ </div>
+
+ {!decision.justification ? undefined : (
+ <div>
+ <label
+ for="comment"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ Description
+ </label>
+ <div class="mt-2">
+ <textarea
+ rows={2}
+ name="comment"
+ id="comment"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ >
+ {decision.justification}
+ </textarea>
+ </div>
+ </div>
+ )}
+
+ {!balanceLimit ? undefined : (
+ <div class="px-4">
+ <div class="flex mt-2 rounded-md w-fit shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div class="whitespace-nowrap pointer-events-none bg-gray-200 inset-y-0 items-center px-3 flex">
+ <i18n.Translate>Max balance</i18n.Translate>
+ </div>
+ <div class="p-2 disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6">
+ <RenderAmount
+ value={Amounts.parseOrThrow(balanceLimit.threshold)}
+ spec={config.currency_specification}
+ />
+ </div>
+ </div>
+ </div>
+ )}
+
+ <div class="w-full px-4 pt-4">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead class="bg-gray-50">
+ <tr>
+ <th
+ scope="col"
+ class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
+ >
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >
+ <i18n.Translate>Timeframe</i18n.Translate>
+ </th>
+ <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200">
+ {decision.limits.rules.map((r) => {
+ if (r.operation_type === "BALANCE") return;
+ return (
+ <tr>
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left">
+ {r.operation_type}
+ </td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ {r.timeframe.d_us === "forever"
+ ? ""
+ : formatDuration(
+ intervalToDuration({
+ start: 0,
+ end: r.timeframe.d_us / 1000,
+ }),
+ )}
+ </td>
+ <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 text-right">
+ <RenderAmount
+ value={Amounts.parseOrThrow(r.threshold)}
+ spec={config.currency_specification}
+ />
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
</div>
);
}
+export function RenderAmount({
+ value,
+ spec,
+ negative,
+ withColor,
+ hideSmall,
+}: {
+ spec: CurrencySpecification;
+ value: AmountJson;
+ hideSmall?: boolean;
+ negative?: boolean;
+ withColor?: boolean;
+}): VNode {
+ const neg = !!negative; // convert to true or false
+
+ const { currency, normal, small } = Amounts.stringifyValueWithSpec(
+ value,
+ spec,
+ );
+
+ return (
+ <span
+ data-negative={withColor ? neg : undefined}
+ class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ {negative ? "- " : undefined}
+ {currency} {normal}{" "}
+ {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
+ </span>
+ );
+}
+
function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode {
switch (state) {
case TalerExchangeApi.AmlState.normal: {
@@ -382,7 +762,7 @@ function ShowTimeline({
"never"
) : (
<time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
- {format(e.when.t_ms, "dd MMM yyyy")}
+ {format(e.when.t_ms, "dd MMM yyyy HH:mm:ss")}
</time>
)}
</div>
@@ -397,6 +777,66 @@ function ShowTimeline({
);
}
+function InputAmount(
+ {
+ currency,
+ name,
+ value,
+ left,
+ onChange,
+ }: {
+ currency: string;
+ name: string;
+ left?: boolean | undefined;
+ value: string | undefined;
+ onChange?: (s: string) => void;
+ },
+ ref: Ref<HTMLInputElement>,
+): VNode {
+ const FRAC_SEPARATOR = ",";
+ const { config } = useExchangeApiContext();
+ return (
+ <div class="mt-2">
+ <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div class="pointer-events-none inset-y-0 flex items-center px-3">
+ <span class="text-gray-500 sm:text-sm">{currency}</span>
+ </div>
+ <input
+ type="number"
+ data-left={left}
+ class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
+ placeholder="0.00"
+ aria-describedby="price-currency"
+ ref={ref}
+ name={name}
+ id={name}
+ autocomplete="off"
+ value={value ?? ""}
+ disabled={!onChange}
+ onInput={(e) => {
+ if (!onChange) return;
+ const l = e.currentTarget.value.length;
+ const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR);
+ if (
+ sep_pos !== -1 &&
+ l - sep_pos - 1 >
+ config.currency_specification.num_fractional_input_digits
+ ) {
+ e.currentTarget.value = e.currentTarget.value.substring(
+ 0,
+ sep_pos +
+ config.currency_specification.num_fractional_input_digits +
+ 1,
+ );
+ }
+ onChange(e.currentTarget.value);
+ }}
+ />
+ </div>
+ </div>
+ );
+}
+
export type Justification<T = Record<string, unknown>> = {
// form values
value: T;
@@ -424,9 +864,9 @@ function parseJustification(
listOfAllKnownForms: FormMetadata[],
):
| OperationOk<{
- justification: Justification;
- metadata: FormMetadata;
- }>
+ justification: Justification;
+ metadata: FormMetadata;
+ }>
| OperationFail<ParseJustificationFail> {
try {
const justification = JSON.parse(s);
@@ -470,3 +910,216 @@ function parseJustification(
};
}
}
+
+const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = (
+ currency,
+) => [
+ {
+ operation_type: "WITHDRAW",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "DEPOSIT",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "AGGREGATE",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "MERGE",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "BALANCE",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "CLOSE",
+ threshold: `${currency}:2000`,
+ timeframe: {
+ d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ ];
+
+const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = (
+ currency,
+) => [
+ {
+ operation_type: "WITHDRAW",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "DEPOSIT",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "AGGREGATE",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "MERGE",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "BALANCE",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "CLOSE",
+ threshold: `${currency}:100`,
+ timeframe: {
+ d_us: 1 * 60 * 60 * 1000 * 1000,
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ ];
+
+const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = (
+ currency,
+) => [
+ {
+ operation_type: "WITHDRAW",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "DEPOSIT",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "AGGREGATE",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "MERGE",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "BALANCE",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ {
+ operation_type: "CLOSE",
+ threshold: `${currency}:0`,
+ timeframe: {
+ d_us: "forever",
+ },
+ measures: ["verboten"],
+ display_priority: 1,
+ exposed: true,
+ is_and_combinator: true,
+ },
+ ];
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index 7801625d0..87f1aed5f 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -117,7 +117,7 @@ export function CaseUpdate({
);
});
- const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const { handler, status } = useFormState<FormType>(shape, initial, (st) => {
const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({
state: st.state === undefined ? i18n.str`required` : undefined,
threshold: !st.threshold ? i18n.str`required` : undefined,
@@ -143,7 +143,7 @@ export function CaseUpdate({
};
});
- const validatedForm = state.status !== "ok" ? undefined : state.result;
+ const validatedForm = status.status !== "ok" ? undefined : status.result;
const submitHandler =
validatedForm === undefined
@@ -157,31 +157,37 @@ export function CaseUpdate({
value: validatedForm,
};
- const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
- {
- justification: JSON.stringify(justification),
- decision_time: TalerProtocolTimestamp.now(),
- h_payto: account,
- new_state: justification.value
- .state as TalerExchangeApi.AmlState,
- new_threshold: Amounts.stringify(
- justification.value.threshold as AmountJson,
- ),
- kyc_requirements: undefined,
- };
+ const decision: Omit<
+ TalerExchangeApi.AmlDecisionRequest,
+ "officer_sig"
+ > = {
+ justification: JSON.stringify(justification),
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: account,
+ keep_investigating: false,
+ new_rules: {
+ custom_measures: {},
+ expiration_time: {
+ t_s: "never",
+ },
+ rules: [],
+ successor_measure: undefined,
+ },
+ properties: {},
+ new_measures: undefined,
+ };
- return api.addDecisionDetails(officer.account, decision);
+ return api.makeAmlDesicion(officer.account, decision);
},
() => {
- window.location.href = privatePages.cases.url({});
+ window.location.href = privatePages.profile.url({});
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.Forbidden:
- case HttpStatusCode.Unauthorized:
return i18n.str`Wrong credentials for "${officer.account}"`;
case HttpStatusCode.NotFound:
- return i18n.str`Officer or account not found`;
+ return i18n.str`The account was not found`;
case HttpStatusCode.Conflict:
return i18n.str`Officer disabled or more recent decision was already submitted.`;
default:
@@ -218,7 +224,7 @@ export function CaseUpdate({
fields={convertUiField(
i18n,
section.fields,
- form,
+ handler,
getConverterById,
)}
/>
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index 22a6d1867..372fb912f 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -21,21 +21,33 @@
import * as tests from "@gnu-taler/web-util/testing";
import { CasesUI as TestedComponent } from "./Cases.js";
-import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
export default {
title: "cases",
};
export const OneRow = tests.createExample(TestedComponent, {
- filter: TalerExchangeApi.AmlState.normal,
- onChangeFilter: () => null,
records: [
{
- current_state: TalerExchangeApi.AmlState.normal,
+ // current_state: TalerExchangeApi.AmlState.normal,
h_payto: "QWEQWEQWEQWE",
rowid: 1,
- threshold: "USD:1" as AmountString,
+ decision_time: {
+ t_s: "never"
+ },
+ is_active: false,
+ limits: {
+ custom_measures: {},
+ expiration_time: {
+ t_s: "never"
+ },
+ rules: [],
+ successor_measure: undefined,
+ },
+ to_investigate: false,
+ justification: undefined,
+ properties: undefined,
+ // threshold: "USD:1" as AmountString,
},
],
});
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index f66eca33f..278d4bac2 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -21,111 +21,98 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- ErrorLoading,
- InputChoiceHorizontal,
Loading,
- UIHandlerId,
- amlStateConverter,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { useCases } from "../hooks/useCases.js";
+import {
+ useCurrentDecisions,
+ useCurrentDecisionsUnderInvestigation,
+} from "../hooks/decisions.js";
import { privatePages } from "../Routing.js";
-import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
-import { undefinedIfEmpty } from "./CreateAccount.js";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { Officer } from "./Officer.js";
type FormType = {
- state: TalerExchangeApi.AmlState;
+ // state: TalerExchangeApi.AmlState;
};
export function CasesUI({
records,
- filter,
- onChangeFilter,
onFirstPage,
onNext,
+ filtered,
}: {
+ filtered: boolean;
onFirstPage?: () => void;
onNext?: () => void;
- filter: TalerExchangeApi.AmlState;
- onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
- records: TalerExchangeApi.AmlRecord[];
+ records: TalerExchangeApi.AmlDecision[];
}): VNode {
const { i18n } = useTranslationContext();
- const [form, status] = useFormState<FormType>(
- [".state"] as Array<UIHandlerId>,
- {
- state: filter,
- },
- (state) => {
- const errors = undefinedIfEmpty<FormErrors<FormType>>({
- state: state.state === undefined ? i18n.str`required` : undefined,
- });
- if (errors === undefined) {
- const result: FormType = {
- state: state.state!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<FormType> = {
- state: state.state,
- };
- return {
- status: "fail",
- result,
- errors,
- };
- },
- );
- useEffect(() => {
- if (status.status === "ok" && filter !== status.result.state) {
- onChangeFilter(status.result.state);
- }
- }, [form?.state?.value]);
+ // const [form, status] = useFormState<FormType>(
+ // [".state"] as Array<UIHandlerId>,
+ // {
+ // // state: filter,
+ // },
+ // (state) => {
+ // const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ // state: state.state === undefined ? i18n.str`required` : undefined,
+ // });
+ // if (errors === undefined) {
+ // const result: FormType = {
+ // state: state.state!,
+ // };
+ // return {
+ // status: "ok",
+ // result,
+ // errors,
+ // };
+ // }
+ // const result: RecursivePartial<FormType> = {
+ // state: state.state,
+ // };
+ // return {
+ // status: "fail",
+ // result,
+ // errors,
+ // };
+ // },
+ // );
+ // useEffect(() => {
+ // if (status.status === "ok" && filter !== status.result.state) {
+ // onChangeFilter(status.result.state);
+ // }
+ // }, [form?.state?.value]);
return (
<div>
<div class="sm:flex sm:items-center">
- <div class="px-2 sm:flex-auto">
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Cases</i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700 w-80">
- <i18n.Translate>
- A list of all the account with the status
- </i18n.Translate>
- </p>
- </div>
- <div class="px-2">
- <InputChoiceHorizontal<FormType, "state">
- name="state"
- label={i18n.str`Filter`}
- handler={form.state}
- converter={amlStateConverter}
- choices={[
- {
- label: i18n.str`Pending`,
- value: "pending",
- },
- {
- label: i18n.str`Frozen`,
- value: "frozen",
- },
- {
- label: i18n.str`Normal`,
- value: "normal",
- },
- ]}
- />
- </div>
+ {filtered ? (
+ <div class="px-2 sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cases under investigation</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700 w-80">
+ <i18n.Translate>
+ A list of all the accounts which are waiting for a deicison to
+ be made.
+ </i18n.Translate>
+ </p>
+ </div>
+ ) : (
+ <div class="px-2 sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cases</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700 w-80">
+ <i18n.Translate>
+ A list of all the known account by the exchange.
+ </i18n.Translate>
+ </p>
+ </div>
+ )}
</div>
<div class="mt-8 flow-root">
<div class="overflow-x-auto">
@@ -148,12 +135,6 @@ export function CasesUI({
>
<i18n.Translate>Status</i18n.Translate>
</th>
- <th
- scope="col"
- class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
- >
- <i18n.Translate>Threshold</i18n.Translate>
- </th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
@@ -172,35 +153,12 @@ export function CasesUI({
</a>
</div>
</td>
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
- {((state: TalerExchangeApi.AmlState): VNode => {
- switch (state) {
- case TalerExchangeApi.AmlState.normal: {
- return (
- <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
- Normal
- </span>
- );
- }
- case TalerExchangeApi.AmlState.pending: {
- return (
- <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
- Pending
- </span>
- );
- }
- case TalerExchangeApi.AmlState.frozen: {
- return (
- <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
- Frozen
- </span>
- );
- }
- }
- })(r.current_state)}
- </td>
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
- {r.threshold}
+ {r.to_investigate ? (
+ <span title="require investigation">
+ <ToInvestigateIcon />
+ </span>
+ ) : undefined}
</td>
</tr>
);
@@ -217,18 +175,14 @@ export function CasesUI({
}
export function Cases() {
- const [stateFilter, setStateFilter] = useState(
- TalerExchangeApi.AmlState.pending,
- );
-
- const list = useCases(stateFilter);
+ const list = useCurrentDecisions();
const { i18n } = useTranslationContext();
if (!list) {
return <Loading />;
}
if (list instanceof TalerError) {
- return <ErrorLoading error={list} />;
+ return <ErrorLoadingWithDebug error={list} />;
}
if (list.type === "fail") {
@@ -238,28 +192,107 @@ export function Cases() {
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account doesn't have access. Request account activation
- sending your public key.
+ This account signature is wrong, contact administrator or create
+ a new one.
</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
- case HttpStatusCode.Unauthorized: {
+ case HttpStatusCode.NotFound: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>This account is not known.</i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.Conflict:
+ {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ return <Officer />;
+ default:
+ assertUnreachable(list);
+ }
+ }
+
+ return (
+ <CasesUI
+ filtered={false}
+ records={list.body}
+ onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
+ onNext={list.isLastPage ? undefined : list.loadNext}
+ // filter={stateFilter}
+ // onChangeFilter={(d) => {
+ // setStateFilter(d);
+ // }}
+ />
+ );
+}
+export function CasesUnderInvestigation() {
+ const list = useCurrentDecisionsUnderInvestigation();
+ const { i18n } = useTranslationContext();
+
+ if (!list) {
+ return <Loading />;
+ }
+ if (list instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={list} />;
+ }
+
+ if (list.type === "fail") {
+ switch (list.case) {
+ case HttpStatusCode.Forbidden: {
return (
<Fragment>
<Attention type="danger" title={i18n.str`Operation denied`}>
<i18n.Translate>
- This account is not allowed to perform list the cases.
+ This account signature is wrong, contact administrator or create
+ a new one.
</i18n.Translate>
</Attention>
<Officer />
</Fragment>
);
}
- case HttpStatusCode.NotFound:
+ case HttpStatusCode.NotFound: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>This account is not known.</i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
case HttpStatusCode.Conflict:
+ {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
return <Officer />;
default:
assertUnreachable(list);
@@ -268,17 +301,41 @@ export function Cases() {
return (
<CasesUI
+ filtered={true}
records={list.body}
onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
onNext={list.isLastPage ? undefined : list.loadNext}
- filter={stateFilter}
- onChangeFilter={(d) => {
- setStateFilter(d);
- }}
+ // filter={stateFilter}
+ // onChangeFilter={(d) => {
+ // setStateFilter(d);
+ // }}
/>
);
}
+// function ToInvestigateIcon(): VNode {
+// return <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6">
+// <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+// </svg>
+// }
+export const ToInvestigateIcon = () => (
+ <svg
+ title="requires investigation"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6 w-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
+ />
+ </svg>
+);
+
export const PeopleIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -313,7 +370,24 @@ export const HomeIcon = () => (
</svg>
);
-function Pagination({
+export const SearchIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
+ />
+ </svg>
+);
+
+export function Pagination({
onFirstPage,
onNext,
}: {
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 87310aa27..328d8459b 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -89,7 +89,9 @@ function createFormValidator(
};
}
-export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined {
+export function undefinedIfEmpty<T extends object | undefined>(
+ obj: T,
+): T | undefined {
if (obj === undefined) return undefined;
return Object.keys(obj).some(
(k) => (obj as Record<string, T>)[k] !== undefined,
@@ -105,7 +107,7 @@ export function CreateAccount(): VNode {
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const [form, status] = useFormState<FormType>(
+ const { handler, status } = useFormState<FormType>(
[".password", ".repeat"] as Array<UIHandlerId>,
{
password: undefined,
@@ -118,7 +120,7 @@ export function CreateAccount(): VNode {
status.status === "fail" || officer.state !== "not-found"
? undefined
: withErrorHandler(
- async () => officer.create(form.password!.value!),
+ async () => officer.create(handler.password!.value!),
() => {},
);
return (
@@ -148,7 +150,7 @@ export function CreateAccount(): VNode {
name="password"
type="password"
required
- handler={form.password}
+ handler={handler.password}
/>
</div>
@@ -158,7 +160,7 @@ export function CreateAccount(): VNode {
name="repeat"
type="password"
required
- handler={form.repeat}
+ handler={handler.repeat}
/>
</div>
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx
new file mode 100644
index 000000000..220fc55ee
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -0,0 +1,724 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AbsoluteTime,
+ assertUnreachable,
+ buildPayto,
+ encodeCrock,
+ hashPaytoUri,
+ HttpStatusCode,
+ parsePaytoUri,
+ PaytoUri,
+ stringifyPaytoUri,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ convertUiField,
+ encodeCrockForURI,
+ getConverterById,
+ InternationalizationAPI,
+ Loading,
+ RenderAllFieldsByUiConfig,
+ Time,
+ UIFormElementConfig,
+ UIHandlerId,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ getShapeFromFields,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { useAccountInformation } from "../hooks/account.js";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useAccountDecisions } from "../hooks/decisions.js";
+import { privatePages } from "../Routing.js";
+import { Pagination, ToInvestigateIcon } from "./Cases.js";
+
+export function Search() {
+ const officer = useOfficer();
+ const { i18n } = useTranslationContext();
+
+ const [paytoUri, setPayto] = useState<PaytoUri | undefined>(undefined);
+
+ const paytoForm = useFormState(
+ getShapeFromFields(paytoTypeField(i18n)),
+ { paytoType: "iban" },
+ createFormValidator(i18n),
+ );
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+
+ return (
+ <div>
+ <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
+ <i18n.Translate>Search account</i18n.Translate>
+ </h1>
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(
+ i18n,
+ paytoTypeField(i18n),
+ paytoForm.handler,
+ getConverterById,
+ )}
+ />
+ </div>
+ </form>
+
+ {paytoForm.status.status !== "ok" ? undefined : paytoForm.status.result
+ .paytoType === "x-taler-bank" ? (
+ <XTalerBankForm onSearch={setPayto} />
+ ) : paytoForm.status.result.paytoType === "iban" ? (
+ <IbanForm onSearch={setPayto} />
+ ) : (
+ <GenericForm onSearch={setPayto} />
+ )}
+ {!paytoUri ? undefined : <ShowResult payto={paytoUri} />}
+ </div>
+ );
+}
+
+function ShowResult({ payto }: { payto: PaytoUri }): VNode {
+ const paytoStr = stringifyPaytoUri(payto)
+ const account = encodeCrock(hashPaytoUri(paytoStr));
+ const { i18n } = useTranslationContext();
+
+ const history = useAccountDecisions(account);
+ if (!history) {
+ return <Loading />
+ }
+ if (history instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={history} />;
+ }
+ if (history.type === "fail") {
+ switch (history.case) {
+ case HttpStatusCode.Forbidden: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account signature is wrong, contact administrator or create
+ a new one.
+ </i18n.Translate>
+ </Attention>
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.Conflict: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ </Fragment>
+ );
+
+ }
+ case HttpStatusCode.NotFound: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>This account is not known.</i18n.Translate>
+ </Attention>
+ </Fragment>
+ );
+ }
+ default: {
+ assertUnreachable(history)
+ }
+ }
+ }
+
+ if (history.body.length) {
+ return <div class="mt-8">
+ <div class="mb-2">
+ <a
+ href={privatePages.caseDetails.url({
+ cid: account,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>
+ Check account details
+ </i18n.Translate>
+ </a>
+ </div>
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <div>
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Account most recent decisions</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="flow-root">
+ <div class="overflow-x-auto">
+ {!history.body.length ? (
+ <div>empty result </div>
+ ) : (
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
+ >
+ <i18n.Translate>When</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
+ >
+ <i18n.Translate>Justification</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {history.body.map((r, idx) => {
+ return (
+ <tr key={r.h_payto} class="hover:bg-gray-100 ">
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ <Time format="dd/MM/yyyy HH:mm" timestamp={AbsoluteTime.fromProtocolTimestamp(r.decision_time)} />
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ {r.justification}
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
+
+
+ {idx === 0 ? <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
+ <i18n.Translate>LATEST</i18n.Translate>
+ </span> : undefined}
+ {r.is_active ? <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
+ <i18n.Translate>ACTIVE</i18n.Translate>
+ </span> : undefined}
+ {r.decision_time ? (
+ <span title="require investigation">
+ <ToInvestigateIcon />
+ </span>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ <Pagination
+ onFirstPage={history.isFirstPage ? undefined : history.loadFirst}
+ onNext={history.isLastPage ? undefined : history.loadNext} />
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ }
+ // const detailsUrl = new URL(, window.location.href)
+ // detailsUrl.searchParams.set("payto", encodeCrockForURI(paytoStr))
+ return <div class="mt-4">
+ <Attention title={i18n.str`Account not found`} type="warning">
+ <i18n.Translate>
+ There is no history known for this account yet.
+ </i18n.Translate>
+ &nbsp;
+ <a
+ href={privatePages.caseDetailsNewAccount.url({
+ cid: account,
+ payto: encodeCrockForURI(paytoStr),
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>
+ You can make a decision for this account anyway.
+ </i18n.Translate>
+ </a>
+ </Attention>
+ </div>
+}
+
+
+function XTalerBankForm({
+ onSearch,
+}: {
+ onSearch: (p: PaytoUri | undefined) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const fields = talerBankFields(i18n);
+ const form = useFormState(
+ getShapeFromFields(fields),
+ {},
+ createTalerBankPaytoValidator(i18n),
+ );
+ const paytoUri =
+ form.status.status === "fail"
+ ? undefined
+ : buildPayto(
+ "x-taler-bank",
+ form.status.result.hostname,
+ form.status.result.account,
+ {
+ "receiver-name": encodeURIComponent(form.status.result.name),
+ },
+ );
+
+ return (
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(i18n, fields, form.handler, getConverterById)}
+ />
+ </div>
+ <button
+ disabled={form.status.status === "fail"}
+ class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ onClick={() => onSearch(paytoUri)}
+ >
+ <i18n.Translate>Search</i18n.Translate>
+ </button>
+ </form>
+ );
+}
+function IbanForm({
+ onSearch,
+}: {
+ onSearch: (p: PaytoUri | undefined) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const fields = ibanFields(i18n);
+ const form = useFormState(
+ getShapeFromFields(fields),
+ {},
+ createIbanPaytoValidator(i18n),
+ );
+ const paytoUri =
+ form.status.status === "fail"
+ ? undefined
+ : buildPayto("iban", form.status.result.account, form.status.result.bic, {
+ "receiver-name": encodeURIComponent(form.status.result.name),
+ });
+
+ return (
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(i18n, fields, form.handler, getConverterById)}
+ />
+ </div>
+ <button
+ disabled={form.status.status === "fail"}
+ class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ onClick={() => onSearch(paytoUri)}
+ >
+ Search
+ </button>
+ </form>
+ );
+}
+function GenericForm({
+ onSearch,
+}: {
+ onSearch: (p: PaytoUri | undefined) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const fields = genericFields(i18n);
+ const form = useFormState(
+ getShapeFromFields(fields),
+ {},
+ createGenericPaytoValidator(i18n),
+ );
+ const paytoUri =
+ form.status.status === "fail"
+ ? undefined
+ : parsePaytoUri(form.status.result.payto);
+ return (
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(i18n, fields, form.handler, getConverterById)}
+ />
+ </div>
+ <button
+ disabled={form.status.status === "fail"}
+ class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ onClick={() => onSearch(paytoUri)}
+ >
+ Search
+ </button>
+ </form>
+ );
+}
+
+interface FormPayto {
+ paytoType: "generic" | "iban" | "x-taler-bank";
+}
+
+function createFormValidator(i18n: InternationalizationAPI) {
+ return function check(
+ state: RecursivePartial<FormValues<FormPayto>>,
+ ): FormStatus<FormPayto> {
+ const errors = undefinedIfEmpty<FormErrors<FormPayto>>({
+ paytoType: !state?.paytoType ? i18n.str`required` : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: FormPayto = {
+ paytoType: state.paytoType! as any,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormPayto> = {
+ paytoType: state?.paytoType,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+interface PaytoUriGenericForm {
+ payto: string;
+}
+
+function createGenericPaytoValidator(i18n: InternationalizationAPI) {
+ return function check(
+ state: RecursivePartial<FormValues<PaytoUriGenericForm>>,
+ ): FormStatus<PaytoUriGenericForm> {
+ const errors = undefinedIfEmpty<FormErrors<PaytoUriGenericForm>>({
+ payto: !state.payto
+ ? i18n.str`required`
+ : parsePaytoUri(state.payto) === undefined
+ ? i18n.str`invalid`
+ : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: PaytoUriGenericForm = {
+ payto: state.payto! as any,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<PaytoUriGenericForm> = {
+ // targetType: state.iban
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+interface PaytoUriIBANForm {
+ account: string;
+ name: string;
+ bic: string;
+}
+
+function createIbanPaytoValidator(i18n: InternationalizationAPI) {
+ return function check(
+ state: RecursivePartial<FormValues<PaytoUriIBANForm>>,
+ ): FormStatus<PaytoUriIBANForm> {
+ const errors = undefinedIfEmpty<FormErrors<PaytoUriIBANForm>>({
+ account: !state.account ? i18n.str`required` : undefined,
+ name: !state.name ? i18n.str`required` : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: PaytoUriIBANForm = {
+ account: state.account!,
+ name: state.name!,
+ bic: state.bic!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<PaytoUriIBANForm> = {
+ account: state.account,
+ name: state.name,
+ bic: state.bic,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+interface PaytoUriTalerBankForm {
+ hostname: string;
+ account: string;
+ name: string;
+}
+function createTalerBankPaytoValidator(i18n: InternationalizationAPI) {
+ return function check(
+ state: RecursivePartial<FormValues<PaytoUriTalerBankForm>>,
+ ): FormStatus<PaytoUriTalerBankForm> {
+ const errors = undefinedIfEmpty<FormErrors<PaytoUriTalerBankForm>>({
+ account: !state.account ? i18n.str`required` : undefined,
+ hostname: !state.hostname ? i18n.str`required` : undefined,
+ name: !state.name ? i18n.str`required` : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: PaytoUriTalerBankForm = {
+ account: state.account!,
+ hostname: state.hostname!,
+ name: state.name!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<PaytoUriTalerBankForm> = {
+ account: state.account,
+ hostname: state.hostname,
+ name: state.name,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+const paytoTypeField: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "paytoType" as UIHandlerId,
+ type: "choiceHorizontal",
+ required: true,
+ choices: [
+ {
+ value: "iban",
+ label: i18n.str`IBAN`,
+ },
+ {
+ value: "x-taler-bank",
+ label: i18n.str`Taler Bank`,
+ },
+ {
+ value: "generic",
+ label: i18n.str`Generic Payto:// URI`,
+ },
+ ],
+ label: i18n.str`Account type`,
+ },
+];
+
+const receiverName: (i18n: InternationalizationAPI) => UIFormElementConfig = (
+ i18n,
+) => ({
+ id: "name" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: i18n.str`Owner's name`,
+ help: i18n.str`It should match the bank account name.`,
+ placeholder: i18n.str`John Doe`,
+});
+
+const genericFields: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "payto" as UIHandlerId,
+ type: "textArea",
+ required: true,
+ label: i18n.str`Payto URI`,
+ help: i18n.str`As defined by RFC 8905`,
+ placeholder: i18n.str`payto://`,
+ },
+];
+const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = (
+ i18n,
+) => [
+ {
+ id: "account" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: i18n.str`Account`,
+ help: i18n.str`International Bank Account Number`,
+ placeholder: i18n.str`DE1231231231`,
+ // validator: (value) => validateIBAN(value, i18n),
+ },
+ receiverName(i18n),
+ {
+ id: "bic" as UIHandlerId,
+ type: "text",
+ label: i18n.str`Bank`,
+ help: i18n.str`Business Identifier Code`,
+ placeholder: i18n.str`GENODEM1GLS`,
+ // validator: (value) => validateIBAN(value, i18n),
+ },
+ ];
+
+const talerBankFields: (
+ i18n: InternationalizationAPI,
+) => UIFormElementConfig[] = (i18n) => [
+ {
+ id: "account" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: i18n.str`Bank account`,
+ help: i18n.str`Bank account id`,
+ placeholder: i18n.str`DE123123123`,
+ },
+ {
+ id: "hostname" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: i18n.str`Hostname`,
+ help: i18n.str`Without the scheme, may include subpath: bank.com, bank.com/path/`,
+ placeholder: i18n.str`bank.demo.taler.net`,
+ // validator: (value) => validateTalerBank(value, i18n),
+ },
+ receiverName(i18n),
+];
+
+function validateIBAN(
+ iban: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString | 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 invalid, checksum is wrong`;
+ return undefined;
+}
+
+const DOMAIN_REGEX =
+ /^[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9-_](?:\.[a-zA-Z0-9-_]{2,})+(:[0-9]+)?(\/[a-zA-Z0-9-.]+)*\/?$/;
+
+function validateTalerBank(
+ addr: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ try {
+ const valid = DOMAIN_REGEX.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid host.`;
+}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
index 714bf6580..2fc661bd4 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -42,7 +42,7 @@ const nullTranslator: InternationalizationAPI = {
};
export const WithEmptyHistory = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([], [], nullTranslator, []),
+ history: getEventsFromAmlHistory([], nullTranslator, []),
until: AbsoluteTime.now(),
});
@@ -50,79 +50,17 @@ export const WithSomeEvents = tests.createExample(TestedComponent, {
history: getEventsFromAmlHistory(
[
{
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208199,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208211,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208220,
- },
- },
- {
- decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- justification:
- '{"index":4,"name":"Declaration for trusts (902.13e)","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700208362854},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"contractingPartner":"f","knownAs":"a","trust":{"name":"b","type":"discretionary","revocability":"irrevocable"}}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700208385,
- },
- },
- {
- decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- justification:
- '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488420810},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"qwe"}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700488423,
- },
- },
- {
- decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- justification:
- '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488671251},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"asd asd asd "}}',
- new_threshold: "STATER:0" as AmountString,
- new_state: 1,
- decision_time: {
- t_s: 1700488677,
- },
- },
- ],
- [
- {
collection_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.subtractDuraction(
AbsoluteTime.now(),
Duration.fromPrettyString("1d"),
),
),
- expiration_time: { t_s: "never" },
- provider_section: "asd",
+ provider_name: "asd",
attributes: {
email: "sebasjm@qwdde.com",
},
+ rowid: 1,
},
],
nullTranslator,
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index cdc5d0bc1..fcec8609a 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -20,16 +20,39 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
- DefaultForm,
FormConfiguration,
+ RenderAllFieldsByUiConfig,
UIFormElementConfig,
UIHandlerId,
- useTranslationContext
+ convertUiField,
+ getConverterById,
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
+import { getShapeFromFields, useFormState } from "../hooks/form.js";
import { AmlEvent } from "./CaseDetails.js";
+/**
+ * the exchange doesn't have a consistent api
+ * https://bugs.gnunet.org/view.php?id=9142
+ *
+ * @param data
+ * @returns
+ */
+function fixProvidedInfo(data: object): object {
+ return Object.entries(data).reduce((prev, [key, value]) => {
+ prev[key] = value;
+ if (typeof value === "object" && value["value"]) {
+ const v = value["value"];
+ if (typeof v === "object" && v["text"]) {
+ prev[key].value = v["text"];
+ }
+ }
+ return prev;
+ }, {} as any);
+}
+
export function ShowConsolidated({
history,
until,
@@ -41,77 +64,77 @@ export function ShowConsolidated({
const cons = getConsolidated(history, until);
- const form: FormConfiguration = {
+ const fixed = fixProvidedInfo(cons.kyc);
+
+ const formConfig: FormConfiguration = {
type: "double-column",
- design: [
+ design: Object.entries(fixed).length > 0 ? [
+
{
- title: i18n.str`AML`,
- fields: [
- {
- type: "amount",
- id: ".aml.threshold" as UIHandlerId,
- currency: "NETZBON",
- label: i18n.str`Threshold`,
- name: "aml.threshold",
- },
- {
- type: "choiceHorizontal",
- label: i18n.str`State`,
- name: "aml.state",
- id: ".aml.state" as UIHandlerId,
- choices: [
- {
- label: i18n.str`Frozen`,
- value: "frozen",
- },
- {
- label: i18n.str`Pending`,
- value: "pending",
- },
- {
- label: i18n.str`Normal`,
- value: "normal",
- },
- ],
- },
- ],
- },
- Object.entries(cons.kyc).length > 0
- ? {
- title: i18n.str`KYC`,
- fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormElementConfig = {
- type: "text",
- label: key as TranslatedString,
- id: `kyc.${key}.value` as UIHandlerId,
- name: `kyc.${key}.value`,
- help: `${field.provider} since ${
- field.since.t_ms === "never"
- ? "never"
- : format(field.since.t_ms, "dd/MM/yyyy")
- }` as TranslatedString,
- };
- return result;
- }),
- }
- : undefined!,
- ],
+ title: i18n.str`KYC collected info`,
+ fields: Object.entries(fixed).map(([key, field]) => {
+ const result: UIFormElementConfig = {
+ type: "text",
+ label: key as TranslatedString,
+ id: `${key}.value` as UIHandlerId,
+ disabled: true,
+ help: `At ${field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy HH:mm:ss")
+ }` as TranslatedString,
+ };
+ return result;
+ }),
+ }
+ ] : [],
};
+ const shape: Array<UIHandlerId> = formConfig.design.flatMap((field) =>
+ getShapeFromFields(field.fields),
+ );
+
+ const { handler } = useFormState<{}>(shape, fixed, (result) => {
+ return { status: "ok", errors: undefined, result };
+ });
+
return (
<Fragment>
- <h1 class="text-base font-semibold leading-7 text-black">
- Consolidated information{" "}
- {until.t_ms === "never"
- ? ""
- : `after ${format(until.t_ms, "dd MMMM yyyy")}`}
- </h1>
- <DefaultForm
- key={`${String(Date.now())}`}
- form={form as any}
- initial={cons}
- readOnly
- onUpdate={() => {}}
- />
+ <div class="space-y-10 divide-y divide-gray-900/10">
+ {formConfig.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={convertUiField(
+ i18n,
+ section.fields,
+ handler,
+ getConverterById,
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
</Fragment>
);
}
@@ -125,7 +148,7 @@ interface Consolidated {
kyc: {
[field: string]: {
value: unknown;
- provider: string;
+ provider?: string;
since: AbsoluteTime;
};
};
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 084e639bf..72656bb98 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -19,7 +19,7 @@ import {
LocalNotificationBanner,
UIHandlerId,
useLocalNotificationHandler,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { FormErrors, useFormState } from "../hooks/form.js";
@@ -36,7 +36,7 @@ export function UnlockAccount(): VNode {
const officer = useOfficer();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const [form, status] = useFormState<FormType>(
+ const { handler, status } = useFormState<FormType>(
[".password"] as Array<UIHandlerId>,
{
password: undefined,
@@ -64,7 +64,7 @@ export function UnlockAccount(): VNode {
status.status === "fail" || officer.state !== "locked"
? undefined
: withErrorHandler(
- async () => officer.tryUnlock(form.password!.value!),
+ async () => officer.tryUnlock(handler.password!.value!),
() => {},
);
@@ -94,14 +94,13 @@ export function UnlockAccount(): VNode {
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
-
<div class="mb-4">
<InputLine<FormType, "password">
label={i18n.str`Password`}
name="password"
type="password"
required
- handler={form.password}
+ handler={handler.password}
/>
</div>
@@ -115,7 +114,6 @@ export function UnlockAccount(): VNode {
<i18n.Translate>Unlock</i18n.Translate>
</Button>
</div>
-
</div>
<Button
type="button"
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 40bdb927e..477ace06b 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-cli",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index c987f0ceb..b03032d22 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-core/src/anastasis-data.ts b/packages/anastasis-core/src/anastasis-data.ts
index 9cbf5f594..e5e178600 100644
--- a/packages/anastasis-core/src/anastasis-data.ts
+++ b/packages/anastasis-core/src/anastasis-data.ts
@@ -11,10 +11,10 @@ export const anastasisData = {
url: "https://v1.anastasis.taler.net/",
name: "Bern University of Applied Sciences, Switzerland",
},
-// {
-// url: "https://v1.anastasis.codeblau.de/",
-// name: "Codeblau GmbH, Germany",
-// },
+ // {
+ // url: "https://v1.anastasis.codeblau.de/",
+ // name: "Codeblau GmbH, Germany",
+ // },
{
url: "https://v1.anastasis.lu/",
name: "Anastasis SARL, Luxembourg",
@@ -213,7 +213,7 @@ export const anastasisData = {
// },
// call_code: "+00",
// },
- ].sort((a, b) => a.name > b.name ? 1 : (a.name < b.name ? -1 : 0)),
+ ].sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)),
},
countryDetails: {
al: {
@@ -498,7 +498,7 @@ export const anastasisData = {
},
widget: "anastasis_gtk_ia_es_ssn",
uuid: "22396a19-f3bb-497e-b63a-961fd639140e",
- "validation-regex": "^[0-9]{11}$",
+ "validation-regex": "^[0-9]{2}/[0-9]{8}/[0-9]{2}[TB]$",
},
],
},
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 9f56489d1..a8f014fe6 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,11 +1,10 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.11.4",
+ "version": "0.13.4",
"license": "MIT",
"type": "module",
"scripts": {
- "build": "./build.mjs",
"compile": "tsc && ./build.mjs",
"dev": "./dev.mjs",
"clean": "rm -rf dist lib tsconfig.tsbuildinfo",
diff --git a/packages/auditor-backoffice-ui/README.md b/packages/auditor-backoffice-ui/README.md
index b10fa6a94..03f4403b4 100644
--- a/packages/auditor-backoffice-ui/README.md
+++ b/packages/auditor-backoffice-ui/README.md
@@ -1,4 +1,4 @@
-## AUditor Admin Frontend
+ ## 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.
diff --git a/packages/auditor-backoffice-ui/dev.mjs b/packages/auditor-backoffice-ui/dev.mjs
index 14d5737de..d2299dad4 100755
--- a/packages/auditor-backoffice-ui/dev.mjs
+++ b/packages/auditor-backoffice-ui/dev.mjs
@@ -18,7 +18,7 @@
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 devEntryPoints = ["src/index.tsx"];
const build = initializeDev({
type: "development",
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
index ce420417c..9cae99136 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,15 +1,14 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.13.4",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
"clean": "rm -rf dist lib tsconfig.tsbuildinfo",
- "build": "./build.mjs",
"check": "tsc",
"compile": "tsc && ./build.mjs",
- "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
+ "dev": "./dev.mjs",
"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",
diff --git a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx b/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
deleted file mode 100644
index 91dec09b0..000000000
--- a/packages/auditor-backoffice-ui/src/AdminRoutes.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- 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
index 3e5cfc273..4188a53a6 100644
--- a/packages/auditor-backoffice-ui/src/Application.tsx
+++ b/packages/auditor-backoffice-ui/src/Application.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -17,6 +17,7 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
@@ -31,11 +32,9 @@ import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js";
import {
NotConnectedAppMenu,
- NotificationCard
+ NotificationCard,
} from "./components/menu/index.js";
-import {
- BackendContextProvider
-} from "./context/backend.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";
@@ -52,17 +51,15 @@ export function Application(): VNode {
/**
* Check connection testing against /config
- *
- * @returns
+ *
+ * @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]);
+ const configData = result.ok && result.data ? result.data : undefined;
+ const ctx = useMemo(() => configData, [configData]);
if (!result.ok) {
if (result.loading) return <Loading />;
@@ -138,26 +135,28 @@ function ApplicationStatusRoutes(): VNode {
);
}
- const SUPPORTED_VERSION = "18: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>
+ const SUPPORTED_VERSION = "1: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`Auditor 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}>
+ <ConfigContextProvider value={ctx!}>
<ApplicationReadyRoutes />
</ConfigContextProvider>
</div>
diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 414eee39d..9e0bda499 100644
--- a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -17,159 +17,54 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-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 { Route, Router } 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 { Loading } from "./components/exception/loading.js";
+import { NotYetReadyAppMenu } from "./components/menu/index.js";
+import { useBackendToken } 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
+ * @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)
+ //TODO FIX bearer
+ const result = useBackendToken();
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ return <LoginPage />;
}
+ const [showSettings, setShowSettings] = useState(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) {
+ if (showSettings) {
return (
<Fragment>
- <NotConnectedAppMenu title="Welcome!" />
- <LoginPage onConfirm={updateToken} />
+ <NotYetReadyAppMenu
+ onShowSettings={() => setShowSettings(true)}
+ title="UI Settings"
+ />
+ <Settings onClose={() => setShowSettings(false)} />
</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}
- />
+ <Route default component={DefaultMainRoute} />
</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}
- />
- );
+ return <InstanceRoutes path={url} />;
}
diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
index 163438654..ab3f3dde3 100644
--- a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -17,225 +17,332 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
- * @author Nic Eigel
+ * @author Nicola 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 { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect, useErrorBoundary, useMemo, useState } from "preact/hooks";
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 { EntityContextProvider } from "./context/entity.js";
+import { AuditorBackend } from "./declaration.js";
+import DefaultList from "./paths/default/index.js";
+import DetailsDashboard from "./paths/details/index.js";
+import FinanceDashboard from "./paths/finance/index.js";
import NotFoundPage from "./paths/notfound/index.js";
-import { Notification } from "./utils/types.js";
-import { LoginToken, MerchantBackend } from "./declaration.js";
+import OperationsDashboard from "./paths/operations/index.js";
+import SecurityDashboard from "./paths/security/index.js";
import { Settings } from "./paths/settings/index.js";
-import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
+import { Notification } from "./utils/types.js";
-export enum InstancePaths {
+export enum Paths {
error = "/error",
settings = "/settings",
- token = "/token",
- inventory_list = "/inventory",
- inventory_update = "/inventory/:pid/update",
- inventory_new = "/inventory/new",
+ key_figures = "/key-figures",
+ critical_errors = "/critical-errors",
+ operating_status = "/operating-status",
+ detail_view = "/detail-view",
+
+ amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies",
+
+ bad_sig_losses_list = "/bad-sig-losses",
+
+ balance_list = "/balance",
+
+ closure_lag_list = "/closure-lags",
+
+ coin_inconsistency_list = "/coin-inconsistencies",
+
+ denomination_key_validity_withdraw_inconsistency_list = "/denomination-key-validity-withdraw-inconsistencies",
+
+ denomination_pending_list = "/denominations-pending",
+
+ denomination_without_sig_list = "/denominations-without-sig",
+
+ deposit_confirmation_list = "/deposit-confirmations",
+ deposit_confirmation_update = "/deposit-confirmation/:rowid/update",
+
+ emergency_list = "/emergencies",
+
+ emergency_by_count_list = "/emergencies-by-count",
+
+ exchange_signkey_list = "/exchange-sign-keys",
+
+ fee_time_inconsistency_list = "/fee-time-inconsistencies",
+
+ historic_denomination_revenue_list = "/historic-denomination-revenues",
+
+ misattribution_in_inconsistency_list = "/misattribution-in-inconsistencies",
+
+ progress_list = "/progress",
+
+ purse_not_closed_inconsistency_list = "/purse-not-closed-inconsistencies",
- deposit_confirmation_list = "/deposit-confirmation",
- deposit_confirmation_update = "/deposit-confirmation/:pid/update",
- deposit_confirmation_new = "/deposit-confirmation/new",
+ purse_list = "/purses",
- interface = "/interface",
+ refresh_hanging_list = "/refreshes-hanging",
+
+ reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies",
+
+ reserve_balance_summary_wrong_inconsistency_list = "/reserve-balance-summary-wrong-inconsistencies",
+
+ reserve_in_inconsistency_list = "/reserve-in-inconsistencies",
+
+ reserve_not_closed_inconsistency_list = "/reserve-not-closed-inconsistencies",
+
+ reserves_list = "/reserves",
+
+ row_inconsistency_list = "/row-inconsistencies",
+
+ row_minor_inconsistency_list = "/row-minor-inconsistencies",
+
+ wire_format_inconsistency_list = "/wire-format-inconsistencies",
+
+ wire_out_inconsistency_list = "/wire-out-inconsistencies",
}
-// eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => { };
+interface TestProps {
+ title: string;
+ endpoint: string;
+ entity: any;
+}
-export enum AdminPaths {
- list_instances = "/instances",
- new_instance = "/instance/new",
- update_instance = "/instance/:id/update",
+function getInstanceTitle(path: string): TestProps {
+ switch (path) {
+ case Paths.key_figures:
+ return { title: `Key figures`, endpoint: "helper", entity: null };
+ case Paths.critical_errors:
+ return { title: `Critical errors`, endpoint: "helper", entity: null };
+ case Paths.operating_status:
+ return { title: `Operating status`, endpoint: "helper", entity: null };
+ case Paths.detail_view:
+ return { title: `Inconsistencies`, endpoint: "helper", entity: null };
+ case Paths.amount_arithmethic_inconsistency_list:
+ let amountArithmeticInconsistency: AuditorBackend.AmountArithmeticInconsistency.ClassAmountArithmeticInconsistency =
+ {} as AuditorBackend.AmountArithmeticInconsistency.ClassAmountArithmeticInconsistency;
+ return {
+ title: `Amount arithmetic inconsistencies`,
+ endpoint: "amount-arithmetic-inconsistency",
+ entity: amountArithmeticInconsistency,
+ };
+ case Paths.bad_sig_losses_list:
+ return {
+ title: `Bad Sig Losses`,
+ endpoint: "bad-sig-losses",
+ entity: null,
+ };
+ case Paths.balance_list:
+ return { title: "Balances", endpoint: "balances", entity: null };
+ case Paths.closure_lag_list:
+ return { title: `Closure Lags`, endpoint: "closure-lags", entity: null };
+ case Paths.coin_inconsistency_list:
+ return {
+ title: `Coin inconsistencies`,
+ endpoint: "coin-inconsistency",
+ entity: null,
+ };
+ case Paths.denomination_key_validity_withdraw_inconsistency_list:
+ return {
+ title: `Denomination key validity withdraw inconsistency`,
+ endpoint: "denomination-key-validity-withdraw-inconsistency",
+ entity: null,
+ };
+ case Paths.denomination_pending_list:
+ return {
+ title: `Denominations pending`,
+ endpoint: "denomination-pending",
+ entity: null,
+ };
+ case Paths.denomination_without_sig_list:
+ return {
+ title: `Denominations without sigs`,
+ endpoint: "denominations-without-sigs",
+ entity: null,
+ };
+ case Paths.deposit_confirmation_list:
+ return {
+ title: "Deposit Confirmations",
+ endpoint: "deposit-confirmation",
+ entity: null,
+ };
+ case Paths.emergency_list:
+ return { title: "Emergencies", endpoint: "emergency", entity: null };
+ case Paths.emergency_by_count_list:
+ return {
+ title: "Emergencies by count",
+ endpoint: "emergency-by-count",
+ entity: null,
+ };
+ case Paths.fee_time_inconsistency_list:
+ return {
+ title: "Fee time inconsistencies",
+ endpoint: "fee-time-inconsistency",
+ entity: null,
+ };
+ case Paths.historic_denomination_revenue_list:
+ return {
+ title: "Historic denomination revenue",
+ endpoint: "historic-denomination-revenue",
+ entity: null,
+ };
+ case Paths.misattribution_in_inconsistency_list:
+ return {
+ title: "Misattribution in inconsistencies",
+ endpoint: "misattribution-in-inconsistency",
+ entity: null,
+ };
+ case Paths.progress_list:
+ return { title: "Progress", endpoint: "progress", entity: null };
+ case Paths.purse_not_closed_inconsistency_list:
+ return {
+ title: "Purse not closed inconsistencies",
+ endpoint: "purse-not-closed-inconsistencies",
+ entity: null,
+ };
+ case Paths.purse_list:
+ return { title: "Purses", endpoint: "purses", entity: null };
+ case Paths.refresh_hanging_list:
+ return {
+ title: "Refreshes hanging",
+ endpoint: "refreshes-hanging",
+ entity: null,
+ };
+ case Paths.reserves_list:
+ return { title: "Reserves", endpoint: "reserves ", entity: null };
+ case Paths.reserve_balance_insufficient_inconsistency_list:
+ return {
+ title: "Reserve balance insufficient inconsistencies",
+ endpoint: "reserve-balance-insufficient-inconsistency",
+ entity: null,
+ };
+ case Paths.reserve_balance_summary_wrong_inconsistency_list:
+ return {
+ title: "Reserve balance summary wrong inconsistencies",
+ endpoint: "reserve-balance-summary-wrong-inconsistency",
+ entity: null,
+ };
+ case Paths.reserve_in_inconsistency_list:
+ return {
+ title: "Reserves in inconsistencies",
+ endpoint: "reserve-in-inconsistency",
+ entity: null,
+ };
+ case Paths.reserve_not_closed_inconsistency_list:
+ return {
+ title: "Reserves not closed inconsistencies",
+ endpoint: "reserve-not-closed-inconsistency",
+ entity: null,
+ };
+ case Paths.row_inconsistency_list:
+ return {
+ title: "Row inconsistencies",
+ endpoint: "row-inconsistency",
+ entity: null,
+ };
+ case Paths.row_minor_inconsistency_list:
+ return {
+ title: "Row minor inconsistencies",
+ endpoint: "row-minor-inconsistencies",
+ entity: null,
+ };
+ case Paths.wire_format_inconsistency_list:
+ let wireFormatInconsistency: AuditorBackend.WireFormatInconsistency.ClassWireFormatInconsistency =
+ {} as AuditorBackend.WireFormatInconsistency.ClassWireFormatInconsistency;
+ return {
+ title: "Wire format inconsistencies",
+ endpoint: "wire-format-inconsistency",
+ entity: wireFormatInconsistency,
+ };
+ case Paths.wire_out_inconsistency_list:
+ return {
+ title: "Wire out inconsistencies",
+ endpoint: "wire-out-inconsistency",
+ entity: null,
+ };
+ case Paths.settings:
+ return { title: `Settings`, endpoint: "settings", entity: null };
+ default:
+ return { title: "", endpoint: "", entity: null };
+ }
}
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;
+export function InstanceRoutes({ path }: Props): VNode {
+ type GlobalNotifState =
+ | (Notification & { to: string | undefined })
+ | 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 [error] = useErrorBoundary();
+ const { title, endpoint, entity } = getInstanceTitle(path);
const value = useMemo(
- () => ({ id, token, admin, changeToken }),
- [id, token, admin],
+ () => ({ title, path, endpoint, entity }),
+ [title, path, endpoint, entity],
);
- 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",
- }}
- />
- </Fragment>
- );
- }
- if (props) {
- return <Next {...props} />;
- }
- return <Next />;
- };
- }
-
- const clearTokenAndGoToRoot = () => {
- route("/");
- // clear all tokens
- updateToken(undefined)
- updateDefaultToken(undefined)
- };
+ //TODO add if needed
+ /*function ServerErrorRedirectTo(to: Paths) {
+ return function ServerErrorRedirectToImpl(
+ error: HttpError<AuditorBackend.ErrorDetail>,
+ ) {
+ if (error.type === ErrorType.TIMEOUT) {
+ setGlobalNotification({
+ message: `The request to the backend take too long and was cancelled`,
+ description: `Diagnostic from ${error.info.url} is "${error.message}"`,
+ type: "ERROR",
+ to,
+ });
+ } else {
+ setGlobalNotification({
+ message: `The backend reported a problem: HTTP status #${error.status}`,
+ description: `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} />;
+ };
+ }*/
return (
- <InstanceContextProvider value={value}>
+ <EntityContextProvider value={value}>
<Menu
- instance={id}
- admin={admin}
+ // instance={id}
+ path={path}
+ title={"Settings"}
onShowSettings={() => {
- route(InstancePaths.interface)
+ route(Paths.settings);
}}
- path={path}
- onLogout={clearTokenAndGoToRoot}
- setInstanceName={setInstanceName}
- isPasswordOk={defaultToken !== undefined}
/>
- <KycBanner />
<NotificationCard notification={globalNotification} />
-
+ {error && (
+ <NotificationCard
+ notification={{
+ message: "Internal error, please report",
+ type: "ERROR",
+ description: (
+ <pre>
+ {
+ (error instanceof Error
+ ? error.stack
+ : String(error)) as TranslatedString
+ }
+ </pre>
+ ),
+ }}
+ />
+ )}
<Router
onChange={(e) => {
const movingOutFromNotification =
@@ -245,140 +352,229 @@ export function InstanceRoutes({
}
}}
>
- {/**
- * 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.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="/" component={Redirect} to={Paths.key_figures} />
+
<Route
- path={InstancePaths.settings}
- component={InstanceUpdatePage}
- onBack={() => {
- route(`/`);
- }}
- onConfirm={() => {
- route(`/`);
- }}
- onUpdateError={noop}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ path={Paths.key_figures}
+ component={FinanceDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
- {/**
- * 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)}
+ path={Paths.critical_errors}
+ component={SecurityDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
<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)}
+ path={Paths.operating_status}
+ component={OperationsDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
<Route
- path={InstancePaths.inventory_new}
- component={ProductCreatePage}
- onConfirm={() => {
- route(InstancePaths.inventory_list);
- }}
- onBack={() => {
- route(InstancePaths.inventory_list);
- }}
+ path={Paths.detail_view}
+ component={DetailsDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_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)}
+ path={Paths.amount_arithmethic_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
<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)}
+ path={Paths.bad_sig_losses_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
<Route
- path={InstancePaths.deposit_confirmation_new}
- component={DepositConfirmationCreatePage}
- onConfirm={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
- onBack={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
+ path={Paths.balance_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.closure_lag_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.coin_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.denomination_key_validity_withdraw_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.denomination_pending_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.denomination_without_sig_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.deposit_confirmation_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.emergency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
/>
- <Route path={InstancePaths.interface} component={Settings} />
- {/**
- * Example pages
- */}
- <Route path="/loading" component={Loading} />
- <Route default component={NotFoundPage} />
+ <Route
+ path={Paths.emergency_by_count_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ {
+ <Route
+ path={Paths.exchange_signkey_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.fee_time_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.historic_denomination_revenue_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.misattribution_in_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.progress_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.purse_not_closed_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.purse_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.refresh_hanging_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.reserve_balance_insufficient_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.reserve_balance_summary_wrong_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.reserve_in_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.reserve_not_closed_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.reserves_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.row_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.row_minor_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.wire_out_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ {
+ <Route
+ path={Paths.wire_format_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ />
+ }
+ <Route path={Paths.settings} component={Settings} />
+
+ {
+ //TODO add if needed
+ /**
+ * Example pages
+ */
+ }
+ {/* <Route path="/loading" component={Loading}/>
+ <Route default component={NotFoundPage}/>*/}
</Router>
- </InstanceContextProvider>
+ </EntityContextProvider>
);
}
@@ -388,98 +584,3 @@ export function Redirect({ to }: { to: string }): null {
});
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/components/exception/AsyncButton.tsx b/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
deleted file mode 100644
index b1fc33877..000000000
--- a/packages/auditor-backoffice-ui/src/components/exception/AsyncButton.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- 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
deleted file mode 100644
index c9340ea76..000000000
--- a/packages/auditor-backoffice-ui/src/components/exception/QR.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- 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
index a043b81eb..5c249f79d 100644
--- a/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
+++ b/packages/auditor-backoffice-ui/src/components/exception/loading.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/components/form/Input.tsx b/packages/auditor-backoffice-ui/src/components/form/Input.tsx
deleted file mode 100644
index c1ddcb064..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/Input.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- 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
deleted file mode 100644
index 4ed4c4b28..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputArray.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- 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
deleted file mode 100644
index f79e16c07..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputBoolean.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- 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
deleted file mode 100644
index b02354d7c..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- 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
deleted file mode 100644
index a398629dc..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputDate.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7aa2703a4..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputDuration.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- 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
deleted file mode 100644
index b5e0bd52b..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputGroup.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- 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
deleted file mode 100644
index b024e2c6b..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputImage.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- 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
deleted file mode 100644
index a2fc8113e..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputLocation.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- 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
deleted file mode 100644
index 3b5df1474..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- 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
deleted file mode 100644
index 6e88e8f2c..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputPayto.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- 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
deleted file mode 100644
index 282e52278..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- 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
deleted file mode 100644
index 32545c89a..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- 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
deleted file mode 100644
index be5800d14..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputSearchOnList.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- 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
deleted file mode 100644
index 12ce6c6aa..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputSecured.stories.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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
deleted file mode 100644
index 9d1a3ab8e..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputSecured.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- 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
deleted file mode 100644
index a8dad5d89..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- 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
deleted file mode 100644
index 668c65ea7..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputStock.stories.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- 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
deleted file mode 100644
index 1d18685c5..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputStock.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2701768aa..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputTab.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- 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
deleted file mode 100644
index b5722e4ec..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputTaxes.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- 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
deleted file mode 100644
index f95dfcd05..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- 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
deleted file mode 100644
index e9fd88770..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- 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
deleted file mode 100644
index a0e1d6ae4..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/JumpToElementById.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-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, placeholder, description }: { placeholder: 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={placeholder}
- />
- {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
deleted file mode 100644
index 03f36dcbb..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/TextField.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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
deleted file mode 100644
index c7559faae..000000000
--- a/packages/auditor-backoffice-ui/src/components/form/useField.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- 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/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx
index 0d53c4d08..4abe465c7 100644
--- a/packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -20,7 +20,7 @@
*/
import { ComponentChildren, createContext, h, VNode } from "preact";
-import { useContext, useMemo } from "preact/hooks";
+import { useMemo } from "preact/hooks";
type Updater<S> = (value: (prevState: S) => S) => void;
@@ -82,16 +82,6 @@ export interface FormType<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]>;
};
@@ -103,7 +93,3 @@ export type FormtoStr<T> = {
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/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
deleted file mode 100644
index 6f5881fc0..000000000
--- a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- 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
index 41fe1374a..a6cd8014d 100644
--- a/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx
+++ b/packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
index 9f1b33893..66469378b 100644
--- a/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
+++ b/packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -55,7 +55,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a>
</div>
- <div class="navbar-menu ">
+ <div class="navbar-menu">
<a
class="navbar-start is-justify-content-center is-flex-grow-1"
href="https://taler.net"
@@ -63,8 +63,10 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
<img src={logo} style={{ height: 35, margin: 10 }} />
</a>
<div class="navbar-end">
- <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- </div>
+ <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
index cfc00148e..0b662d8de 100644
--- a/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -15,57 +15,20 @@
*/
/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
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()
+export function Sidebar(props: any): VNode {
+ const configData = useConfigContext();
const { i18n } = useTranslationContext();
- const kycStatus = useInstanceKYCDetails();
- const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
+ console.log(configData);
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>
@@ -75,210 +38,66 @@ export function Sidebar({
class="is-size-7 has-text-right"
style={{ lineHeight: 0, marginTop: -10 }}
>
- {VERSION} ({config.version})
+ (Version {configData.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">
+ <Fragment>
+ <ul class="menu-list">
+ <li>
+ <a href={"/key-figures"} class="has-icon">
<span class="icon">
<i class="mdi mdi-bank" />
</span>
- <span class="menu-item-label">
- <i18n.Translate>Bank account</i18n.Translate>
+ <span class="menu-item-label">
+ <i18n.Translate>Key figures</i18n.Translate>
</span>
- </a>
- </li>
- <li>
- <a href={"/otp-devices"} class="has-icon">
+ </a>
+ </li>
+ <li>
+ <a href={"/critical-errors"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-lock" />
+ <i class="mdi mdi-alert" />
</span>
- <span class="menu-item-label">
- <i18n.Translate>OTP Devices</i18n.Translate>
+ <span class="menu-item-label">
+ <i18n.Translate>Critical errors</i18n.Translate>
</span>
- </a>
- </li>
- <li>
- <a href={"/reserves"} class="has-icon">
+ </a>
+ </li>
+ <li>
+ <a href={"/operating-status"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-cash" />
+ <i class="mdi mdi-close-network" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Operating status</i18n.Translate>
</span>
- <span class="menu-item-label">Reserves</span>
- </a>
- </li>
- <li>
- <a href={"/webhooks"} class="has-icon">
+ </a>
+ </li>
+ <li>
+ <a href={"/detail-view"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-format-wrap-tight" />
</span>
- <span class="menu-item-label">
- <i18n.Translate>Webhooks</i18n.Translate>
+ <span class="menu-item-label">
+ <i18n.Translate>Inconsistencies</i18n.Translate>
</span>
- </a>
- </li>
- <li>
- <a href={"/settings"} class="has-icon">
+ </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">
+ <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>
+ </li>
+ </ul>
+ </Fragment>
</div>
</aside>
);
-}
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/components/menu/index.tsx b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
index 015d3bd05..214b6bd3b 100644
--- a/packages/auditor-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
@@ -1,61 +1,111 @@
/*
- 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.
+This file is part of GNU Taler
+(C) 2021-2024 Taler Systems S.A.
- 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.
+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.
- 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/>
+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 { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { AdminPaths } from "../../AdminRoutes.js";
-import { InstancePaths } from "../../InstanceRoutes.js";
+import { Paths } 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 {
+function getInstanceTitle(path: string): string {
switch (path) {
- case InstancePaths.settings:
- return `${id}: Settings`;
- 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.interface:
- return `${id}: Interface`;
+ case Paths.key_figures:
+ return "Key figures";
+ case Paths.critical_errors:
+ return "Critical errors";
+ case Paths.operating_status:
+ return "Operating status";
+ case Paths.detail_view:
+ return "Inconsistencies";
+ case Paths.amount_arithmethic_inconsistency_list:
+ return `Amount arithmetic inconsistencies`;
+ case Paths.bad_sig_losses_list:
+ return `Bad sig losses`;
+ case Paths.balance_list:
+ return `Balances`;
+ case Paths.closure_lag_list:
+ return `Closure Lags`;
+ case Paths.coin_inconsistency_list:
+ return `Coin inconsistencies`;
+ case Paths.denomination_key_validity_withdraw_inconsistency_list:
+ return `Denomination key validity withdraw inconsistency`;
+ case Paths.denomination_pending_list:
+ return `Denominations pending`;
+ case Paths.denomination_without_sig_list:
+ return `Denominations without sigs`;
+ case Paths.deposit_confirmation_list:
+ return `Deposit confirmations`;
+ case Paths.deposit_confirmation_update:
+ return `Update deposit confirmation`;
+ case Paths.emergency_list:
+ return `Emergencies`;
+ case Paths.emergency_by_count_list:
+ return `Emergencies by count`;
+ case Paths.exchange_signkey_list:
+ return `Exchange signkeys`;
+ case Paths.fee_time_inconsistency_list:
+ return `Fee time inconsistencies`;
+ case Paths.historic_denomination_revenue_list:
+ return `Historic denomination revenue`;
+ case Paths.misattribution_in_inconsistency_list:
+ return `Misattribution in inconsistencies`;
+ case Paths.progress_list:
+ return `Progress`;
+ case Paths.purse_not_closed_inconsistency_list:
+ return `Purse not closed inconsistencies`;
+ case Paths.purse_list:
+ return `Purses`;
+ case Paths.refresh_hanging_list:
+ return `Refreshes hanging`;
+ case Paths.reserve_balance_insufficient_inconsistency_list:
+ return `Reserve balance insufficient inconsistencies`;
+ case Paths.reserve_balance_summary_wrong_inconsistency_list:
+ return `Reserve balance summary wrong inconsistencies`;
+ case Paths.reserve_in_inconsistency_list:
+ return `Reserves in inconsistencies`;
+ case Paths.reserve_not_closed_inconsistency_list:
+ return `Reserves not closed inconsistencies`;
+ case Paths.row_inconsistency_list:
+ return `Row inconsistencies`;
+ case Paths.row_minor_inconsistency_list:
+ return `Row minor inconsistencies`;
+ case Paths.wire_format_inconsistency_list:
+ return `Wire format inconsistencies`;
+ case Paths.wire_out_inconsistency_list:
+ return `Wire out inconsistencies`;
+ case Paths.settings:
+ return `Settings`;
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({
@@ -71,25 +121,9 @@ function WithTitle({
return <Fragment>{children}</Fragment>;
}
-export function Menu({
- onLogout,
- onShowSettings,
- title,
- instance,
- path,
- admin,
- setInstanceName,
- isPasswordOk
-}: MenuProps): VNode {
+export function Menu({ onShowSettings, title, path }: 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;
+ const titleWithSubtitle = getInstanceTitle(path.replace("app/#", ""));
return (
<WithTitle title={titleWithSubtitle}>
<div
@@ -101,40 +135,7 @@ export function Menu({
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>
- )}
+ <Sidebar onShowSettings={onShowSettings} mobile={mobileOpen} />
</div>
</WithTitle>
);
@@ -143,13 +144,12 @@ export function Menu({
interface NotYetReadyAppMenuProps {
title: string;
onShowSettings: () => void;
- onLogout?: () => void;
- isPasswordOk: boolean;
}
interface NotifProps {
notification?: Notification;
}
+
export function NotificationCard({
notification: n,
}: NotifProps): VNode | null {
@@ -186,6 +186,7 @@ export function NotificationCard({
interface NotConnectedAppMenuProps {
title: string;
}
+
export function NotConnectedAppMenu({
title,
}: NotConnectedAppMenuProps): VNode {
@@ -209,10 +210,8 @@ export function NotConnectedAppMenu({
}
export function NotYetReadyAppMenu({
- onLogout,
onShowSettings,
title,
- isPasswordOk
}: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -229,9 +228,13 @@ export function NotYetReadyAppMenu({
onMobileMenu={() => setMobileOpen(!mobileOpen)}
title={title}
/>
- {onLogout && (
- <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
- )}
+ (
+ <Sidebar
+ onShowSettings={onShowSettings}
+ instance=""
+ mobile={mobileOpen}
+ />
+ )
</div>
);
}
diff --git a/packages/auditor-backoffice-ui/src/components/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
index 8372c84cc..b8e3a43a3 100644
--- a/packages/auditor-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -19,14 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+/**
+ * Imports.
+ */
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;
@@ -95,402 +92,3 @@ export function ConfirmModal({
</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
deleted file mode 100644
index 073382fb1..000000000
--- a/packages/auditor-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- 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
deleted file mode 100644
index af594de0f..000000000
--- a/packages/auditor-backoffice-ui/src/components/notifications/Notifications.stories.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- 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
deleted file mode 100644
index 235c75577..000000000
--- a/packages/auditor-backoffice-ui/src/components/notifications/index.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0bc629d46..000000000
--- a/packages/auditor-backoffice-ui/src/components/picker/DatePicker.tsx
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8f74d55ac..000000000
--- a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.stories.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- 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
deleted file mode 100644
index ba003cce5..000000000
--- a/packages/auditor-backoffice-ui/src/components/picker/DurationPicker.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2d5a54cde..000000000
--- a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- 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
deleted file mode 100644
index 377d9c1ba..000000000
--- a/packages/auditor-backoffice-ui/src/components/product/InventoryProductForm.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- 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
deleted file mode 100644
index c6d280f94..000000000
--- a/packages/auditor-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- 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
deleted file mode 100644
index e91e8c876..000000000
--- a/packages/auditor-backoffice-ui/src/components/product/ProductForm.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- 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
deleted file mode 100644
index 25751dd96..000000000
--- a/packages/auditor-backoffice-ui/src/components/product/ProductList.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- 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
deleted file mode 100644
index 359859819..000000000
--- a/packages/auditor-backoffice-ui/src/context/backend.test.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- 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
index b13b92c42..0d041f069 100644
--- a/packages/auditor-backoffice-ui/src/context/backend.ts
+++ b/packages/auditor-backoffice-ui/src/context/backend.ts
@@ -17,43 +17,21 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-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";
+import { useBackendURL } from "../hooks/index.js";
interface BackendContextType {
- url: string,
- alreadyTriedLogin: boolean;
- token?: LoginToken;
- updateToken: (token: LoginToken | undefined) => void;
+ url: string;
}
const BackendContext = createContext<BackendContextType>({
url: "",
- alreadyTriedLogin: false,
- token: undefined,
- updateToken: () => null,
});
-function useBackendContextState(
- defaultUrl?: string,
-): BackendContextType {
-const [url] = useBackendURL(defaultUrl);
- //const url = "http://localhost:8081";
- const [token, updateToken] = useBackendDefaultToken();
-
- return {
- url,
- token,
- alreadyTriedLogin: token !== undefined,
- updateToken,
- };
-}
-
export const BackendContextProvider = ({
children,
defaultUrl,
@@ -61,10 +39,26 @@ export const BackendContextProvider = ({
children: any;
defaultUrl?: string;
}): VNode => {
- const value = useBackendContextState(defaultUrl);
+ const [url] = useBackendURL(defaultUrl);
- return h(BackendContext.Provider, { value, children });
+ return h(BackendContext.Provider, {
+ value: {
+ url,
+ },
+ children,
+ });
};
export const useBackendContext = (): BackendContextType =>
useContext(BackendContext);
+
+interface BackendTokenType {
+ token: string;
+}
+
+const BackendTokenContext = createContext<BackendTokenType>({} as any);
+
+export const BackendTokenContextProvider = BackendTokenContext.Provider;
+
+export const useBackendTokenContext = (): BackendTokenType =>
+ useContext(BackendTokenContext);
diff --git a/packages/auditor-backoffice-ui/src/context/config.ts b/packages/auditor-backoffice-ui/src/context/config.ts
index def45ea64..58ee5a594 100644
--- a/packages/auditor-backoffice-ui/src/context/config.ts
+++ b/packages/auditor-backoffice-ui/src/context/config.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
+ (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
@@ -21,12 +21,9 @@
import { createContext } from "preact";
import { useContext } from "preact/hooks";
+import { AuditorBackend } from "../declaration.js";
-interface Type {
- currency: string;
- version: string;
-}
-const Context = createContext<Type>(null!);
+const Context = createContext<AuditorBackend.VersionResponse>(null!);
export const ConfigContextProvider = Context.Provider;
-export const useConfigContext = (): Type => useContext(Context);
+export const useConfigContext = (): AuditorBackend.VersionResponse => useContext(Context);
diff --git a/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx b/packages/auditor-backoffice-ui/src/context/entity.ts
index 9a445eb32..8181931c4 100644
--- a/packages/auditor-backoffice-ui/src/components/form/useGroupField.tsx
+++ b/packages/auditor-backoffice-ui/src/context/entity.ts
@@ -17,25 +17,31 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-import { useFormContext } from "./FormProvider.js";
+import { createContext } from "preact";
+import { useContext } from "preact/hooks";
-interface Use {
- hasError?: boolean;
+interface EntityType {
+ title: string;
+ path: string;
+ endpoint: string;
+ entity: any;
}
-export function useGroupField<T>(name: keyof T): Use {
- const f = useFormContext<T>();
- if (!f) return {};
+const EntityContext = createContext<EntityType>({} as any);
- return {
- hasError: readField(f.errors, String(name)),
- };
+export const EntityContextProvider = EntityContext.Provider;
+
+export const useEntityContext = (): EntityType => useContext(EntityContext);
+
+interface EntityDataType {
+ data: any;
}
-const readField = (object: any, name: string) => {
- return name
- .split(".")
- .reduce((prev, current) => prev && prev[current], object);
-};
+const EntityDataContext = createContext<EntityDataType>({} as any);
+
+export const EntityDataContextProvider = EntityDataContext.Provider;
+
+export const useEntityDataContext = (): EntityDataType => useContext(EntityDataContext);
diff --git a/packages/auditor-backoffice-ui/src/custom.d.ts b/packages/auditor-backoffice-ui/src/custom.d.ts
index 34522a2dd..e693c2951 100644
--- a/packages/auditor-backoffice-ui/src/custom.d.ts
+++ b/packages/auditor-backoffice-ui/src/custom.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/declaration.d.ts b/packages/auditor-backoffice-ui/src/declaration.d.ts
index 0c6f599f7..69839ef05 100644
--- a/packages/auditor-backoffice-ui/src/declaration.d.ts
+++ b/packages/auditor-backoffice-ui/src/declaration.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
+ (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
@@ -17,145 +17,13 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-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.
@@ -193,1601 +61,78 @@ export namespace AuditorBackend {
// 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 /deposit-confirmation/$SERIAL_ID
- interface DepositConfirmationDetail {
- serial_id: string;
- timestamp: string;
- refund_deadline: string;
- wire_deadline: string;
- amount_without_fee: string;
- }
- }
-
-}
-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;
+ name: "taler-auditor";
+ version: string;
- // How long does the frontend intend to hold the lock
- duration: RelativeTime;
+ // Default (!) currency supported by this backend.
+ // This is the currency that the backend should
+ // suggest by default to the user when entering
+ // amounts. See currencies for a list of
+ // supported currencies and how to render them.
+ implementation: string;
+ currency: string;
+ auditor_public_key: string;
+ exchange_master_public_key: string;
- // How many units should be locked?
- quantity: Integer;
- }
+ // How services should render currencies supported
+ // by this backend. Maps
+ // currency codes (e.g. "EUR" or "KUDOS") to
+ // the respective currency specification.
+ // All currencies in this map are supported by
+ // the backend. Note that the actual currency
+ // specifications are a *hint* for applications
+ // that would like *advice* on how to render amounts.
+ // Applications *may* ignore the currency specification
+ // if they know how to render currencies that they are
+ // used with.
+ //currencies: { currency: CurrencySpecification };
- // DELETE /private/products/$PRODUCT_ID
+ // Array of exchanges trusted by the merchant.
+ // Since protocol v6.
+ // exchanges: ExchangeConfigInfo[];
}
- 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;
+ namespace AmountArithmeticInconsistency {
+ class ClassAmountArithmeticInconsistency {
+ data: AmountArithmeticInconsistencyDetail[];
}
- 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 SummaryResponse {
+ amount_arithmetic_inconsistency: AmountArithmeticInconsistencyDetail[];
}
- 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
+ interface AmountArithmeticInconsistencyDetail {
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;
+ operation: string;
+ exchange_amount: string;
+ auditor_amount: string;
+ profitable: boolean;
+ suppressed: boolean;
}
}
- 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;
+ namespace WireFormatInconsistency {
+ class ClassWireFormatInconsistency {
+ data: WireFormatInconsistencyDetail[];
}
- interface OtpDeviceSummaryResponse {
- // Array of devices that are present in our backend.
- otp_devices: OtpDeviceEntry[];
+ interface SummaryResponse {
+ responseData: WireFormatInconsistencyDetail[];
}
- 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 WireFormatInconsistencyDetail {
+ row_id: number;
+ amount: string;
+ wire_offset: string;
+ diagnostic: string;
+ suppressed: boolean;
}
}
-
- 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
deleted file mode 100644
index f22badc88..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/async.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- 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
index 8d99546a8..69b63e02b 100644
--- a/packages/auditor-backoffice-ui/src/hooks/backend.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -17,12 +17,13 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+/**
+ * Imports.
+ */
import {
- ErrorType,
- HttpError,
HttpResponse,
HttpResponseOk,
RequestError,
@@ -32,9 +33,7 @@ import {
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";
-
+import { AuditorBackend } from "../declaration.js";
export function useMatchMutate(): (
re?: RegExp,
@@ -49,78 +48,111 @@ export function useMatchMutate(): (
}
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,
- });
+ 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 CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
+const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+
+export function useBackendConfig(): HttpResponse<
+ AuditorBackend.VersionResponse | undefined,
+ RequestError<AuditorBackend.ErrorDetail>
> {
const { request } = useBackendBaseRequest();
- type Type = MerchantBackend.Instances.InstancesResponse;
-
- const [result, setResult] = useState<
- HttpResponse<Type, MerchantBackend.ErrorDetail>
- >({ loading: true });
+ type Type = AuditorBackend.VersionResponse;
+ type State = {
+ data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>;
+ timer: number;
+ };
+ const [result, setResult] = useState<State>({
+ data: { loading: true },
+ timer: 0,
+ });
useEffect(() => {
- request<Type>(`/management/instances`)
- .then((data) => setResult(data))
- .catch((error: RequestError<MerchantBackend.ErrorDetail>) =>
- setResult(error.cause),
- );
+ 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;
+ return result.data;
}
-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>
+export function useBackendToken(): HttpResponse<
+ AuditorBackend.VersionResponse,
+ RequestError<AuditorBackend.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 });
+ type Type = AuditorBackend.VersionResponse;
+ type State = {
+ data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>;
+ timer: number;
+ };
+ const [result, setResult] = useState<State>({
+ data: { loading: true },
+ timer: 0,
+ });
useEffect(() => {
if (result.timer) {
- clearTimeout(result.timer)
+ clearTimeout(result.timer);
}
- function tryConfig(): void {
- request<Type>(`/config`)
+
+ function tryToken(): void {
+ request<Type>(`/monitoring/balances`)
.then((data) => {
const timer: any = setTimeout(() => {
- tryConfig()
- }, CHECK_CONFIG_INTERVAL_OK)
- setResult({ data, timer })
+ tryToken();
+ }, 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 })
+ tryToken();
+ }, CHECK_CONFIG_INTERVAL_FAIL);
+ const data = error.cause;
+ setResult({ data, timer });
});
}
- tryConfig()
+
+ tryToken();
}, [request]);
return result.data;
@@ -132,35 +164,9 @@ interface useBackendInstanceRequestType {
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>>;
+ multiFetcher: <T>(params: string[]) => Promise<HttpResponseOk<T>[]>;
}
+
interface useBackendBaseRequestType {
request: <T>(
endpoint: string,
@@ -168,310 +174,74 @@ interface useBackendBaseRequestType {
) => 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
*/
+//TODO: Add token
export function useBackendBaseRequest(): useBackendBaseRequestType {
- const { url: backend, token: loginToken } = useBackendContext();
+ const { url: backend } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const token = loginToken?.token;
const request = useCallback(
function requestImpl<T>(
endpoint: string,
+ //todo: remove
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
- return res
- }).catch(err => {
- throw err
- });
+ return requestHandler<T>(backend, endpoint, { ...options })
+ .then((res) => {
+ return res;
+ })
+ .catch((err) => {
+ throw err;
+ });
},
- [backend, token],
+ [backend],
);
return { request };
}
-export function useBackendInstanceRequest(): useBackendInstanceRequestType {
- const { url: rootBackendUrl, token: rootToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
+export function useBackendRequest(): useBackendInstanceRequestType {
+ const { url: baseUrl } = useBackendContext();
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 });
+ return requestHandler<T>(baseUrl, endpoint, { ...options });
},
- [baseUrl, token],
+ [baseUrl],
);
const multiFetcher = useCallback(
function multiFetcherImpl<T>(
- args: [endpoints: string[]],
+ params: string[],
+ options: RequestOptions = {},
): Promise<HttpResponseOk<T>[]> {
- const [endpoints] = args
return Promise.all(
- endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { token }),
+ params.map((endpoint) =>
+ requestHandler<T>(baseUrl, endpoint, { ...options }),
),
);
},
- [baseUrl, token],
+ [baseUrl],
);
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 });
+ return requestHandler<T>(baseUrl, endpoint, {});
},
- [baseUrl, token],
+ [baseUrl],
);
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
deleted file mode 100644
index 03b064646..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/bank.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- 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/critical.ts b/packages/auditor-backoffice-ui/src/hooks/critical.ts
new file mode 100644
index 000000000..8283fefbb
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/critical.ts
@@ -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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend } from "../declaration.js";
+import { useBackendRequest } 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;
+
+type YesOrNo = "yes" | "no";
+
+export interface HelperDashboardFilter {
+ finance?: YesOrNo;
+ security?: YesOrNo;
+ operating?: YesOrNo;
+ detail?: YesOrNo;
+}
+
+export function getCriticalData(
+ args?: HelperDashboardFilter,
+ updateFilter?: (d: Date) => void,
+): HttpResponse<any, AuditorBackend.ErrorDetail> {
+ const { multiFetcher } = useBackendRequest();
+ const endpoints = [
+ "monitoring/fee-time-inconsistency",
+ "monitoring/emergency",
+ "monitoring/emergency-by-count",
+ "monitoring/reserve-balance-insufficient-inconsistency",
+ ];
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<any>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >(endpoints, multiFetcher, {
+ refreshInterval: 60,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+
+ if (list) {
+ return { ok: true, data: [list] };
+ }
+ 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
deleted file mode 100644
index e4ec9a2f2..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- 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/entity.ts b/packages/auditor-backoffice-ui/src/hooks/entity.ts
new file mode 100644
index 000000000..3cfdd8616
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/entity.ts
@@ -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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend } from "../declaration.js";
+import { useBackendRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+import { useEntityContext } from "../context/entity.js";
+
+const useSWR = _useSWR as unknown as SWRHook;
+
+interface Props {
+ endpoint: string;
+ entity: any;
+}
+
+export function getEntityList({
+ endpoint,
+ entity,
+}: Props): HttpResponse<any, AuditorBackend.ErrorDetail> {
+ const { fetcher } = useBackendRequest();
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<typeof entity>,
+ RequestError<AuditorBackend.ErrorDetail>
+ >([`monitoring/` + endpoint], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+
+ if (list?.data != null) {
+ return { ok: true, data: [list?.data] };
+ }
+ return { loading: true };
+}
+export interface EntityAPI {
+ updateEntity: (id: string) => Promise<void>;
+}
+
+export function useEntityAPI(): EntityAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendRequest();
+ const { endpoint } = useEntityContext();
+ const data = { suppressed: true };
+
+ const updateEntity = async (id: string): Promise<void> => {
+ const r = await request(`monitoring/${endpoint}/${id}`, {
+ method: "PATCH",
+ data,
+ });
+
+ return await mutateAll(/.*\/monitoring.*/);
+ };
+
+ return { updateEntity };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/finance.ts b/packages/auditor-backoffice-ui/src/hooks/finance.ts
new file mode 100644
index 000000000..a0d035735
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/finance.ts
@@ -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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend } from "../declaration.js";
+import { useBackendRequest } 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 getKeyFiguresData(): HttpResponse<
+ any,
+ AuditorBackend.ErrorDetail
+> {
+ const { multiFetcher } = useBackendRequest();
+ const endpoints = [
+ "monitoring/misattribution-in-inconsistency",
+ "monitoring/coin-inconsistency",
+ "monitoring/reserve-in-inconsistency",
+ "monitoring/bad-sig-losses",
+ "monitoring/balances",
+ "monitoring/amount-arithmetic-inconsistency",
+ "monitoring/wire-format-inconsistency",
+ "monitoring/wire-out-inconsistency",
+ "monitoring/reserve-balance-summary-wrong-inconsistency",
+ ];
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<any>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >(endpoints, multiFetcher, {
+ refreshInterval: 60,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+
+ if (list) {
+ return { ok: true, data: [list] };
+ }
+ return { loading: true };
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts
index 61afbc94a..b844c49cf 100644
--- a/packages/auditor-backoffice-ui/src/hooks/index.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/index.ts
@@ -14,112 +14,41 @@
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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import { StateUpdater, useState } from "preact/hooks";
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 canonUrl = canonicalizeBaseUrl(url ?? calculateRootPath());
-
-export function useBackendURL(
- url?: string,
-): [string, StateUpdater<string>] {
- const [value, setter] = useSimpleLocalStorage(
- "auditor-base-url",
- url || calculateRootPath(),
- );
+ const [value, setter] = useSimpleLocalStorage("auditor-base-url", canonUrl);
const checkedSetter = (v: ValueOrFunction<string>) => {
- return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
+ // FIXME: Explain?!
+ 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];
-}
+function calculateRootPath() {
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
-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>];
+ /*
+ * By default, auditor backend serves the html content
+ * from the /webui root. This should cover most of the
+ * cases and the rootPath will be the auditor backend
+ * URL where the instances are
+ */
+ return rootPath.replace("/spa/", "");
}
-export function useSimpleLocalStorage(
+function useSimpleLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
diff --git a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts b/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
deleted file mode 100644
index ee1576764..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/instance.test.ts
+++ /dev/null
@@ -1,741 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0677191db..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/instance.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- 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
deleted file mode 100644
index d101f7bb8..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/listener.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- 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
deleted file mode 100644
index 133ddd80b..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/notifications.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- 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/operational.ts b/packages/auditor-backoffice-ui/src/hooks/operational.ts
new file mode 100644
index 000000000..c40a1423c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/operational.ts
@@ -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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend } from "../declaration.js";
+import { useBackendRequest } 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;
+
+type YesOrNo = "yes" | "no";
+
+export interface HelperDashboardFilter {
+ finance?: YesOrNo;
+ security?: YesOrNo;
+ operating?: YesOrNo;
+ detail?: YesOrNo;
+}
+
+export function getOperationData(
+ args?: HelperDashboardFilter,
+ updateFilter?: (d: Date) => void,
+): HttpResponse<any, AuditorBackend.ErrorDetail> {
+ const { multiFetcher } = useBackendRequest();
+ const endpoints = [
+ "monitoring/row-inconsistency",
+ "monitoring/purse-not-closed-inconsistencies",
+ "monitoring/reserve-not-closed-inconsistency",
+ "monitoring/denominations-without-sigs",
+ "monitoring/deposit-confirmation",
+ "monitoring/denomination-key-validity-withdraw-inconsistency",
+ "monitoring/refreshes-hanging",
+ //TODO fix endpoint
+ // "monitoring/closure-lags",
+ // "monitoring/row-minor-inconsistencies",
+ // "monitoring/historic-denomination-revenue",
+ // "monitoring/denomination-pending",
+ "monitoring/historic-reserve-summary",
+ ];
+
+ const { data: list, error: listError } = useSWR<
+ HttpResponseOk<any>[],
+ RequestError<AuditorBackend.ErrorDetail>
+ >(endpoints, multiFetcher, {
+ refreshInterval: 60,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ });
+
+ if (listError) return listError.cause;
+
+ if (list) {
+ return { ok: true, data: [list] };
+ }
+ return { loading: true };
+}
+
+export interface EntityAPI {
+ updateEntity: (id: string) => Promise<void>;
+}
diff --git a/packages/auditor-backoffice-ui/src/hooks/order.test.ts b/packages/auditor-backoffice-ui/src/hooks/order.test.ts
deleted file mode 100644
index c243309a8..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/order.test.ts
+++ /dev/null
@@ -1,587 +0,0 @@
-/*
- 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
deleted file mode 100644
index e7a893f2c..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/order.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- 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
deleted file mode 100644
index b045e365a..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/otp.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7cac10e25..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/product.test.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8ca8d2724..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/product.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- 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, 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) => `/private/products/${p.product_id}`,
- );
- const { data: products, error: productError } = useSWR<
- HttpResponseOk<MerchantBackend.Products.ProductDetail>[],
- RequestError<MerchantBackend.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
deleted file mode 100644
index b3eecd754..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/reserve.test.ts
+++ /dev/null
@@ -1,448 +0,0 @@
-/*
- 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
deleted file mode 100644
index b719bfbe6..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/reserves.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- 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
deleted file mode 100644
index ee8728cc8..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/templates.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7955f832a..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/testing.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- 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, TalerRevenueHttpClient, TalerWireGatewayHttpClient } 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 = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient)
- const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient)
- const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient)
-
- 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
deleted file mode 100644
index a7187af27..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/transfer.test.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- 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
deleted file mode 100644
index 27c3bdc75..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/transfer.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- 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
deleted file mode 100644
index b6485259f..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/urls.ts
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- 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/webhooks.ts b/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
deleted file mode 100644
index ad6bf96e2..000000000
--- a/packages/auditor-backoffice-ui/src/hooks/webhooks.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- 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/poheader b/packages/auditor-backoffice-ui/src/i18n/poheader
index 7ddcf49b8..b06d54a53 100644
--- a/packages/auditor-backoffice-ui/src/i18n/poheader
+++ b/packages/auditor-backoffice-ui/src/i18n/poheader
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2021-2023 Taler Systems S.A.
+# (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
diff --git a/packages/auditor-backoffice-ui/src/i18n/strings-prelude b/packages/auditor-backoffice-ui/src/i18n/strings-prelude
index 6c68662de..e8b8297be 100644
--- a/packages/auditor-backoffice-ui/src/i18n/strings-prelude
+++ b/packages/auditor-backoffice-ui/src/i18n/strings-prelude
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/i18n/strings.ts b/packages/auditor-backoffice-ui/src/i18n/strings.ts
index 65dc41358..d3fb99b29 100644
--- a/packages/auditor-backoffice-ui/src/i18n/strings.ts
+++ b/packages/auditor-backoffice-ui/src/i18n/strings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
index 5ef56ca05..7324d3de6 100644
--- a/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
+++ b/packages/auditor-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2021-2023 Taler Systems S.A.
+# (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
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
deleted file mode 100644
index 91b6b4b56..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- 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
deleted file mode 100644
index d13b7e929..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- 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
deleted file mode 100644
index c620c6482..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- 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
deleted file mode 100644
index 23f41ecff..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0012f9b9b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- 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/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
deleted file mode 100644
index 885a351d2..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- 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
deleted file mode 100644
index e0f5d5430..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- 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
deleted file mode 100644
index b59112338..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2f839291b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- 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/default/Table.tsx b/packages/auditor-backoffice-ui/src/paths/default/Table.tsx
new file mode 100644
index 000000000..05db3fc43
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/default/Table.tsx
@@ -0,0 +1,148 @@
+/*
+ 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 {
+ useEntityContext,
+ useEntityDataContext,
+} from "../../context/entity.js";
+
+interface Props {
+ onSuppress: (id: any) => void;
+}
+
+export function CardTable({ onSuppress }: Props): any {
+ const data = useEntityDataContext();
+ const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ const { title, endpoint, entity } = useEntityContext();
+
+ 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>{title}</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options"></div>
+ </header>
+ <div class="card-content">
+ <div class="b-table has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {data.data[0][endpoint] !== undefined &&
+ data.data[0][endpoint].length != 0 ? (
+ <Table data={data.data[0][endpoint]} onSuppress={onSuppress} />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+interface TableProps {
+ data: any;
+ onSuppress: (id: any) => void;
+}
+
+function Table({ data, onSuppress }: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ const { entity } = useEntityContext();
+ type Entity = typeof entity;
+ let count = 0;
+
+ return (
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ {Object.keys(data[0]).map((i: Entity) => {
+ const paramName =
+ i[0].toUpperCase() + i.replace("_", " ").slice(1, i.count);
+ return (
+ <Fragment key={count.toString() + i}>
+ <th>
+ <i18n.Translate>{paramName}</i18n.Translate>
+ </th>
+ </Fragment>
+ );
+ })}
+ </tr>
+ </thead>
+ <tbody>
+ {data.map((key: Entity, value: string) => {
+ return (
+ <tr>
+ {Object.keys(data[0]).map((i: Entity) => {
+ return (
+ <Fragment>
+ <td>{key[i] == false ? "false" : key[i]}</td>
+ </Fragment>
+ );
+ })}
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`suppress`}
+ >
+ <button
+ class="button is-small is-success "
+ type="button"
+ onClick={(): void => onSuppress(key["row_id"])}
+ >
+ {<i18n.Translate>Suppress</i18n.Translate>}
+ </button>
+ </span>
+ </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-happy mdi-48px" />
+ </span>
+ </p>
+ <p>
+ <i18n.Translate>There are no entries yet</i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/default/index.tsx b/packages/auditor-backoffice-ui/src/paths/default/index.tsx
new file mode 100644
index 000000000..1bcd17b1c
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/default/index.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 Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { route } from "preact-router";
+import { useMemo, useState } from "preact/hooks";
+import { Loading } from "../../components/exception/loading.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { ConfirmModal } from "../../components/modal/index.js";
+import {
+ EntityDataContextProvider,
+ useEntityContext,
+} from "../../context/entity.js";
+import { AuditorBackend, WithId } from "../../declaration.js";
+import { getEntityList, useEntityAPI } from "../../hooks/entity.js";
+import { Paths } from "../../InstanceRoutes.js";
+import { Notification } from "../../utils/types.js";
+import { CardTable } from "./Table.js";
+
+interface Props {
+ onNotFound: () => VNode;
+ onLoadError: (e: HttpError<AuditorBackend.ErrorDetail>) => VNode;
+}
+
+export default function DefaultList({ onLoadError, onNotFound }: Props): VNode {
+ const { endpoint, entity } = useEntityContext();
+ const result = getEntityList({ endpoint, entity });
+ const { updateEntity } = useEntityAPI();
+ const [suppressing, setSuppressing] = useState<
+ (typeof entity & 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 onNotFound();
+ return onLoadError(result);
+ }
+
+ let data = result.data;
+ const value = useMemo(() => ({ data }), [data]);
+
+ function onReturn(): void {
+ route(Paths.detail_view);
+ }
+
+ return (
+ <section class="section is-main-section">
+ <button class="button is-fullwidth" onClick={onReturn}>
+ Back
+ </button>
+ <br />
+
+ <NotificationCard notification={notif} />
+
+ <EntityDataContextProvider value={value}>
+ <CardTable
+ onSuppress={(e: typeof entity & WithId) => setSuppressing(e)}
+ />
+ </EntityDataContextProvider>
+
+ {suppressing && (
+ <ConfirmModal
+ label={`Suppress row`}
+ description={`Suppress the row`}
+ danger
+ active
+ onCancel={() => setSuppressing(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await updateEntity(suppressing);
+ setNotif({
+ message: i18n.str`Entity row with id: ${suppressing} has been suppressed`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to suppress row`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setSuppressing(null);
+ }}
+ >
+ <p class="warning">
+ Suppressing a row <b>cannot be undone</b> in this GUI.
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx
new file mode 100644
index 000000000..602ce5ef0
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx
@@ -0,0 +1,383 @@
+/*
+ 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 { route } from "preact-router";
+import { Paths } from "../../InstanceRoutes.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;
+
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+
+ onCreate: () => void;
+}
+
+export function ListPage(): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.amount_arithmethic_inconsistency_list)
+ }
+ value={"Amount arithmetic inconsistencies"}
+ >
+ Amount arithmetic inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.bad_sig_losses_list)}
+ value={"Bad signature losses"}
+ >
+ Bad signature losses
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.closure_lag_list)}
+ >
+ Closure Lags
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.coin_inconsistency_list)}
+ >
+ Coin inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(
+ Paths.denomination_key_validity_withdraw_inconsistency_list,
+ )
+ }
+ >
+ Denominations key validity
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.denomination_without_sig_list)}
+ >
+ Denominations without signature
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.denomination_pending_list)}
+ >
+ Denominations pending
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.deposit_confirmation_list)}
+ >
+ Deposit confirmations
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.emergency_list)}
+ >
+ Emergencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.emergency_by_count_list)}
+ >
+ Emergencies by count
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.fee_time_inconsistency_list)}
+ >
+ Fee time inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.misattribution_in_inconsistency_list)
+ }
+ >
+ Misattribution in inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.purse_not_closed_inconsistency_list)
+ }
+ >
+ Purses not closed
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.purse_list)}
+ >
+ Purses
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.refresh_hanging_list)}
+ >
+ Refreshes hanging
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.reserve_balance_insufficient_inconsistency_list)
+ }
+ >
+ Reserve balances insufficient
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.reserve_balance_summary_wrong_inconsistency_list)
+ }
+ >
+ Reserve balances summary wrong
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.reserve_in_inconsistency_list)}
+ >
+ Reserves in
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) =>
+ route(Paths.reserve_not_closed_inconsistency_list)
+ }
+ >
+ Reserves not closed
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.reserves_list)}
+ >
+ Reserves
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.row_inconsistency_list)}
+ >
+ Row inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.row_minor_inconsistency_list)}
+ >
+ Row minor inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.wire_format_inconsistency_list)}
+ >
+ Wire format inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="column">
+ <div class="card">
+ <div class="card-body">
+ <button
+ class="button is-fullwidth"
+ onClick={(e) => route(Paths.wire_out_inconsistency_list)}
+ >
+ Wire out inconsistencies
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/details/index.tsx
new file mode 100644
index 000000000..163ccd3c9
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/details/index.tsx
@@ -0,0 +1,38 @@
+/*
+ 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 Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { NotificationCard } from "../../components/menu/index.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
+
+export default function DetailsDashboard(): VNode {
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+ <ListPage />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx
new file mode 100644
index 000000000..4a05ae851
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx
@@ -0,0 +1,272 @@
+/*
+ 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 Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+/**
+ * Imports.
+ */
+import { Fragment, h, VNode } from "preact";
+
+export function ListPage(data: any): VNode {
+ let balances = data.data.data[0][4].data.balances;
+ let coinBalances = [
+ "Total recoup loss",
+ "Coin refund fee revenue",
+ "Coin deposit fee revenue",
+ "Coin melt fee revenue",
+ "Coin irregular loss",
+ "Coins reported emergency risk by amount",
+ "Coins emergencies loss by count",
+ "Coins emergencies loss",
+ "Coins total arithmetic delta minus",
+ "Coins total arithmetic delta plus",
+ "Total escrowed",
+ "Total refresh hanging",
+ ];
+ let reserveBalances = [
+ "Total balance summary delta minus",
+ "Total balance reserve not closed",
+ "Reserves total arithmetic delta minus",
+ "Reserves total arithmetic delta plus",
+ "Reserves total bad signature loss",
+ "Reserves history fee revenue",
+ "Reserves open fee revenue",
+ ];
+ let i = 0;
+
+ return (
+ <Fragment>
+ <div class="columns">
+ <div class="column is-half">
+ <div class="columns">
+ <div class="column">
+ <div class="card">
+ <div class="card-content">
+ <table class="table is-striped is-fullwidth is-dark">
+ <tbody>
+ <tr>
+ <th>Finding</th>
+ <td class="has-text-right">
+ <b>Count</b>
+ </td>
+ <td class="has-text-right">
+ <b>Gain/Loss</b>
+ </td>
+ </tr>
+ {data["data"]["data"][0].map((x: any) => {
+ const key = Object.keys(x.data)[0];
+ let value = Object.values(x.data)[0];
+ const paramName =
+ key[0].toUpperCase() +
+ key
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .slice(1, key.length);
+ if (key == "balances") {
+ //TODO fix
+ let gains = 0;
+ if (value == null) value = 0;
+ else value = Object.keys(value).length;
+
+ return (
+ <tr class="is-link">
+ <td>{paramName}</td>
+ <td class="has-text-right">
+ <p
+ class={
+ value == 0 ? "text-success" : "text-danger"
+ }
+ >
+ {String(value)}
+ </p>
+ </td>
+ <td class="has-text-right">
+ <p
+ class={
+ gains == 0 ? "text-success" : "text-danger"
+ }
+ >
+ {String(gains)}
+ </p>
+ </td>
+ </tr>
+ );
+ } else {
+ <tr class="is-link">
+ <td>{paramName}</td>
+ <td class="has-text-right">
+ <p
+ class={
+ value == 0 ? "text-success" : "text-danger"
+ }
+ >
+ {String(value)}
+ </p>
+ </td>
+ <td class="has-text-right">
+ <p>
+ {
+ //TODO
+ }
+ </p>
+ </td>
+ </tr>;
+ }
+ })}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="card">
+ <div class="card-content">
+ <table class="table is-striped is-fullwidth is-dark">
+ <tbody>
+ <tr>
+ <th>Summary</th>
+ <td class="has-text-right">
+ <b>Value</b>
+ </td>
+ </tr>
+ <tr>
+ <td>Total gain/loss</td>
+ <td class="has-text-right">
+ {
+ //TODO fix
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>Pending gain/loss</td>
+ <td class="has-text-right">
+ {
+ //TODO fix
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>Transaction count</td>
+ <td class="has-text-right">
+ {
+ //TODO fix
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>Transactions pending</td>
+ <td class="has-text-right">
+ {
+ //TODO fix
+ }
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="column is-half">
+ <div class="card">
+ <div class="card-content">
+ <p class="has-text-weight-bold">Helper coin</p>
+ <table class="table is-striped is-fullwidth is-dark">
+ <tbody>
+ <tr>
+ <th>Balance</th>
+ <td>
+ <b>Value</b>
+ </td>
+ </tr>
+ {balances.map((x: any) => {
+ let key = x.balance_key;
+ let balanceName =
+ key[0].toUpperCase() +
+ key
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .slice(1, key.length);
+
+ if (coinBalances.includes(balanceName)) {
+ let value = balances[i].balance_value.replace(":", " ");
+ i = i + 1;
+ return (
+ <tr class="is-link">
+ <td>{balanceName}</td>
+ <td>
+ <p>{value}</p>
+ </td>
+ </tr>
+ );
+ } else {
+ return null;
+ }
+ })}
+ </tbody>
+ </table>
+ <p class="has-text-weight-bold">Helper reserve</p>
+ <table class="table is-striped is-fullwidth is-dark">
+ <tbody>
+ <tr>
+ <th>Balance</th>
+ <td>
+ <b>Value</b>
+ </td>
+ </tr>
+ {balances.map((x: any) => {
+ let key = x.balance_key;
+ let balanceName =
+ key[0].toUpperCase() +
+ key
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .slice(1, key.length);
+
+ if (reserveBalances.includes(balanceName)) {
+ let value = balances[i].balance_value.replace(":", " ");
+ i = i + 1;
+ return (
+ <tr class="is-link">
+ <td>{balanceName}</td>
+ <td>
+ <p>{value}</p>
+ </td>
+ </tr>
+ );
+ } else {
+ return null;
+ }
+ })}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/finance/index.tsx
index 5b93ac169..13c718886 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/finance/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,29 +16,40 @@
/**
*
+ * @author Nic Eigel
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, useTranslationContext } 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 { useState } from "preact/hooks";
+import { Loading } from "../../components/exception/loading.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { getKeyFiguresData } from "../../hooks/finance.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;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
}
-export default function ListKYC({
+export default function FinanceDashboard({
onUnauthorized,
- onLoadError,
+ // onLoadError,
+ onCreate,
+ onSelect,
onNotFound,
}: Props): VNode {
- const result = useInstanceKYCDetails();
+ const result = getKeyFiguresData();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
+
if (result.loading) return <Loading />;
if (!result.ok) {
if (
@@ -51,13 +62,13 @@ export default function ListKYC({
result.status === HttpStatusCode.NotFound
)
return onNotFound();
- return onLoadError(result);
+ else return onNotFound();
}
- const status = result.data.type === "ok" ? undefined : result.data.status;
-
- if (!status) {
- return <div>no kyc required</div>;
- }
- return <ListPage status={status} />;
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+ <ListPage data={result} />
+ </section>
+ );
}
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
deleted file mode 100644
index 6e4786a47..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7d33d25ce..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- 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/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
deleted file mode 100644
index 24da755b9..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7d6db0782..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
+++ /dev/null
@@ -1,385 +0,0 @@
-/*
- 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
deleted file mode 100644
index 100241e22..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- 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
deleted file mode 100644
index d6b1d65e0..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0d20879e8..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- 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`doesn't 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
deleted file mode 100644
index 44dee7651..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2fc0819bb..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- 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/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
deleted file mode 100644
index 573064aea..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- 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">
- </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">
- </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">
- </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
deleted file mode 100644
index 99599cfab..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/index.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- 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 { createDepositConfirmation } = useDepositConfirmationAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { i18n } = useTranslationContext();
-
- return (
- <Fragment>
- <NotificationCard notification={notif} />
- </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
deleted file mode 100644
index 41c297d5b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/List.stories.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- 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;
-}
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
deleted file mode 100644
index a99cfd2ef..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/index.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- 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`}
- placeholder={i18n.str`serial id`}
- />
-
- {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
deleted file mode 100644
index a85b13b8b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/Update.stories.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- 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
deleted file mode 100644
index 97715171e..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/UpdatePage.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8e0f7647f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- 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
deleted file mode 100644
index 21dadb1e3..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/details/DetailPage.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- 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
deleted file mode 100644
index 9b393b818..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- 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
deleted file mode 100644
index 367fabce2..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- 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/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
deleted file mode 100644
index d33f64ada..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- 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
deleted file mode 100644
index 338081886..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- 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/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
deleted file mode 100644
index bd9f65718..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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
deleted file mode 100644
index 62ceaa24b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
+++ /dev/null
@@ -1,705 +0,0 @@
-/*
- 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 default 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
deleted file mode 100644
index 88a984c97..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2474fd042..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- 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
deleted file mode 100644
index 6e73a01a5..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- 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
deleted file mode 100644
index 5ff76e37a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
+++ /dev/null
@@ -1,770 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8c863f386..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- 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
deleted file mode 100644
index 1517a3c42..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- 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
deleted file mode 100644
index 156c577f4..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- 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
deleted file mode 100644
index 9f80719a1..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- 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
deleted file mode 100644
index b2806bb79..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx
+++ /dev/null
@@ -1,417 +0,0 @@
-/*
- 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
deleted file mode 100644
index 92e714fb8..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- 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`}
- placeholder={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/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
deleted file mode 100644
index ffeaa064a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- 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 { isRfc3548Base32Charset, randomRfc3548Base32Key } from "@gnu-taler/taler-util";
-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 { InputSelector } from "../../../../components/form/InputSelector.js";
-import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
-import { useBackendContext } from "../../../../context/backend.js";
-import { MerchantBackend } from "../../../../declaration.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`
- : !isRfc3548Base32Charset(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: randomRfc3548Base32Key(),
- }));
- }}
- >
- <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
deleted file mode 100644
index db3842711..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- 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/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
deleted file mode 100644
index b18049674..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index 4efee9781..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- 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/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
deleted file mode 100644
index 2aae8738a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- 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/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
deleted file mode 100644
index 85bb272aa..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- 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 { randomRfc3548Base32Key } from "@gnu-taler/taler-util";
-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 { InputSelector } from "../../../../components/form/InputSelector.js";
-import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
-import { MerchantBackend, WithId } from "../../../../declaration.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: randomRfc3548Base32Key(),
- }));
- }}
- >
- <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
deleted file mode 100644
index 52f6c6c29..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- 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
deleted file mode 100644
index 2fc0819bb..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- 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/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
deleted file mode 100644
index 6b02430cc..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0c30ff14c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/index.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- 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, 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.ProductDetail;
-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} />
-
- </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
deleted file mode 100644
index c2c4d548c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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
deleted file mode 100644
index 275f855cb..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/Table.tsx
+++ /dev/null
@@ -1,496 +0,0 @@
-/*
- 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
deleted file mode 100644
index 34b21daa2..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/list/index.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- 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`}
- placeholder={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
deleted file mode 100644
index a85b13b8b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- 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
deleted file mode 100644
index 97715171e..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8e0f7647f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- 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/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
deleted file mode 100644
index e46941b6d..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- 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
deleted file mode 100644
index 445ca3ef0..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- 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
deleted file mode 100644
index 1d512c843..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- 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
deleted file mode 100644
index 4bbaf1459..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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
deleted file mode 100644
index d8840eeac..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- 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
deleted file mode 100644
index 41c715f20..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- 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
deleted file mode 100644
index 491028695..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- 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 { 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 = "not-supported";
- 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/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
deleted file mode 100644
index e205ee621..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- 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
deleted file mode 100644
index b78236bc7..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- 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
deleted file mode 100644
index b070bbde3..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- 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
deleted file mode 100644
index 795e7ec82..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- 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
deleted file mode 100644
index b26ff0000..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- 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
deleted file mode 100644
index c9d17ea3b..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index 502cfea08..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- 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, TalerMerchantApi } 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 { InputTab } from "../../../../components/form/InputTab.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";
-
-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<TalerMerchantApi.TemplateContractDetails>),
- };
-
- 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
deleted file mode 100644
index a29ee53b6..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/create/index.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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
deleted file mode 100644
index 702e9ba4a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index bf6062c34..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- 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
deleted file mode 100644
index 9fdf4ead9..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/Table.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- 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
deleted file mode 100644
index c7927b772..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/list/index.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- 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`}
- placeholder={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
deleted file mode 100644
index eb853c8ff..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- 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
deleted file mode 100644
index f2276b0c4..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- 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/update/UpdatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
deleted file mode 100644
index 2b73536fb..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- 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, TalerMerchantApi } 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 { InputTab } from "../../../../components/form/InputTab.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";
-
-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<TalerMerchantApi.TemplateContractDetails>),
- };
-
- 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
deleted file mode 100644
index 3adca45db..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/index.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- 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
deleted file mode 100644
index 13576d94d..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- 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
deleted file mode 100644
index 983804d3e..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- 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
deleted file mode 100644
index ed1242ef5..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/use/index.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- 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
deleted file mode 100644
index 549e7581f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- 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 oldToken = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined;
- const newToken = `secret-token:${form.new_token}` as AccessToken;
- onNewToken(oldToken, newToken)
- }
-
- 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 oldToken = `secret-token:${form.old_token}` as AccessToken;
- onClearToken(oldToken)
- } 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
deleted file mode 100644
index 22365c9e1..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/token/index.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- 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
deleted file mode 100644
index 5f0f56f2d..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/token/stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index 64b67335c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- 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
deleted file mode 100644
index 13f5f3c12..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- 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/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
deleted file mode 100644
index 92b3f9853..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- 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
deleted file mode 100644
index 02b12c4c2..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- 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
deleted file mode 100644
index b6b1cf328..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- 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
deleted file mode 100644
index 0fdbb9bc3..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- 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/update/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
deleted file mode 100644
index 817a7025c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/update/Update.stories.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- 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
deleted file mode 100644
index a27a0cb06..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- 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
deleted file mode 100644
index e44cf5c0f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/update/index.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- 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
deleted file mode 100644
index 4857ede97..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index bfa2a883e..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- 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 corresponding 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
deleted file mode 100644
index 924e6d9b8..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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
deleted file mode 100644
index 702e9ba4a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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
deleted file mode 100644
index 87e221e3c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- 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
deleted file mode 100644
index 42a179d2c..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- 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
deleted file mode 100644
index a6f6f1511..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8d07cb31f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- 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
deleted file mode 100644
index 76a23b6e5..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- 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
deleted file mode 100644
index 3f723ed87..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- 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
index 1c98b7c9b..16fb18fdf 100644
--- a/packages/auditor-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/login/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,63 +16,65 @@
/**
*
+ * @author Nic Eigel
* @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();
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { Route } from "preact-router";
+import { useCallback, useState } from "preact/hooks";
+import { NotificationCard } from "../../components/menu/index.js";
+import { useBackendTokenContext } from "../../context/backend.js";
+import { useBackendToken } from "../../hooks/backend.js";
+import { Paths, Redirect } from "../../InstanceRoutes.js";
+import { Notification } from "../../utils/types.js";
+
+export function LoginPage(): VNode {
const [token, setToken] = useState("");
-
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
+ const doLogin = useCallback(
+ async function doLoginImpl() {
+ const result = useBackendToken();
+ if (!result.ok) {
+ }
+ if (result.ok) {
+ //TODO fixme
+ const { token } = useBackendTokenContext();
+ /* return (
+ <Route path="/" component={Redirect} to={Paths.key_figures}/>
+ );*/
+ } else {
+ setNotif({
+ message: "Your password is incorrect",
+ type: "ERROR",
+ });
+ }
+ },
+ [token],
+ );
+ return <Route path="/" component={Redirect} to={Paths.key_figures} />;
- 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" }}>
+ 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>
+ <p class="modal-card-title">{i18n.str`Token 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>
+ <i18n.Translate>
+ Need the access token for the API.
+ </i18n.Translate>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
@@ -88,11 +90,7 @@ export function LoginPage({ onConfirm }: Props): VNode {
type="password"
placeholder={"current access token"}
name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? doLogin()
- : null
- }
+ onKeyPress={(e) => (e.keyCode === 13 ? doLogin() : null)}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
@@ -109,94 +107,106 @@ export function LoginPage({ onConfirm }: Props): VNode {
borderTop: 0,
}}
>
- <AsyncButton
- onClick={doLogin}
- >
+ <AsyncButton onClick={() => doLogin()}>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</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>
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <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 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>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "space-between",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <div />
- <AsyncButton
- type="is-info"
- onClick={doLogin}
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
>
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </footer>
+ <div />
+ <AsyncButton type="is-info" onClick={doLogin}>
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
</div>
</div>
- </div>
+ </Fragment>
);
}
-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>
+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
index 061a67025..68adb79bf 100644
--- a/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/notfound/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx
new file mode 100644
index 000000000..3b26ff071
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.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 Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+/**
+ * Imports.
+ */
+import { Fragment, h, VNode } from "preact";
+
+export function ListPage(data: any): VNode {
+ return (
+ <Fragment>
+ <div class="columns is-fullwidth">
+ <div class="column is-fullwidth">
+ <div class="card">
+ <div class="card-content">
+ <table class="table is-striped is-fullwidth">
+ <tbody>
+ <tr>
+ <th>Finding</th>
+ <td class="has-text-right">
+ <b>Count</b>
+ </td>
+ <td class="has-text-right">
+ <b>Time difference (s)</b>
+ </td>
+ <td class="has-text-right">
+ <b>Diagnostic</b>
+ </td>
+ </tr>
+ {data["data"]["data"][0].map((x: any) => {
+ const key = Object.keys(x.data)[0];
+ let value = Object.values(x.data)[0];
+ console.log(value);
+ if (!!value) value = 0;
+ const paramName =
+ key[0].toUpperCase() +
+ key
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .slice(1, key.length);
+ return (
+ <tr class="is-link">
+ <td>{paramName}</td>
+ <td className="has-text-right">
+ <p
+ class={value == 0 ? "text-success" : "text-danger"}
+ >
+ {String(value)}
+ </p>
+ </td>
+ <td className="has-text-right">
+ {
+ //TODO
+ }
+ </td>
+ <td>
+ {
+ //TODO
+ }
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/operations/index.tsx
index 7db7478f7..da8374b20 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/operations/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,46 +16,40 @@
/**
*
+ * @author Nic Eigel
* @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";
+import { ErrorType, 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 { getOperationData } from "../../hooks/operational.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
-export type Entity = MerchantBackend.Transfers.TransferInformation;
interface Props {
- onBack?: () => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
- onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- tid: string;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
}
-export default function TemplateQrPage({
- tid,
- onBack,
- onLoadError,
- onNotFound,
+export default function OperationsDashboard({
onUnauthorized,
+ // onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
}: Props): VNode {
- const result = useTemplateDetails(tid);
+ const result = getOperationData();
+
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
if (result.loading) return <Loading />;
if (!result.ok) {
if (
@@ -68,13 +62,13 @@ export default function TemplateQrPage({
result.status === HttpStatusCode.NotFound
)
return onNotFound();
- return onLoadError(result);
+ else return onNotFound();
}
return (
- <>
+ <section class="section is-main-section">
<NotificationCard notification={notif} />
- <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
- </>
+ <ListPage data={result} />
+ </section>
);
}
diff --git a/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx
new file mode 100644
index 000000000..4408dd7f6
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx
@@ -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 Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+
+export function ListPage(data: any): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="columns is-fullwidth">
+ <div class="column is-fullwidth">
+ <div class="card">
+ <div class="card-content">
+ <table class="table is-striped is-fullwidth">
+ <tbody>
+ <tr>
+ <th>Finding</th>
+ <td class="has-text-right">
+ <b>Count</b>
+ </td>
+ <td class="has-text-right">
+ <b>Expiration dates</b>
+ </td>
+ </tr>
+ {data["data"]["data"][0].map((x: any) => {
+ const key = Object.keys(x.data)[0];
+ let value = Object.values(x.data)[0];
+ console.log(value);
+ if (!!value) value = 0;
+ const paramName =
+ key[0].toUpperCase() +
+ key
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .slice(1, key.length);
+ return (
+ <tr class="is-link">
+ <td>{paramName}</td>
+ <td class="has-text-right">
+ <p
+ class={value == 0 ? "text-success" : "text-danger"}
+ >
+ {String(value)}
+ </p>
+ </td>
+ <td class="has-text-right">
+ {
+ //TODO
+ }
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </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/security/index.tsx
index 8e2a74529..873494352 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/security/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,35 +16,42 @@
/**
*
+ * @author Nic Eigel
* @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";
+/**
+ * Imports.
+ */
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, 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 { getCriticalData } from "../../hooks/critical.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
interface Props {
- rid: string;
-
onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
onNotFound: () => VNode;
- onDelete: () => void;
- onBack: () => void;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
}
-export default function DetailReserve({
- rid,
+
+export default function SecurityDashboard({
onUnauthorized,
- onLoadError,
+ // onLoadError,
+ onCreate,
+ onSelect,
onNotFound,
- onBack,
- onDelete,
}: Props): VNode {
- const result = useReserveDetails(rid);
+ const result = getCriticalData();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+
+ const { i18n } = useTranslationContext();
if (result.loading) return <Loading />;
if (!result.ok) {
@@ -58,11 +65,13 @@ export default function DetailReserve({
result.status === HttpStatusCode.NotFound
)
return onNotFound();
- return onLoadError(result);
+ else return onNotFound();
}
+
return (
- <Fragment>
- <DetailPage selected={result.data} onBack={onBack} id={rid} />
- </Fragment>
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+ <ListPage data={result} />
+ </section>
);
}
diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
index 093c3d09d..3d07f4cf8 100644
--- a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
@@ -1,8 +1,34 @@
+/*
+ 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
+ */
+
+/**
+ * Imports.
+ */
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 {
+ FormErrors,
+ FormProvider,
+} from "../../components/forms/FormProvider.js";
import { LangSelector } from "../../components/menu/LangSelector.js";
import { Settings, useSettings } from "../../hooks/useSettings.js";
@@ -14,99 +40,78 @@ function getBrowserLang(): string | undefined {
}
export function Settings({ onClose }: { onClose?: () => void }): VNode {
- const { i18n } = useTranslationContext()
- const borwserLang = getBrowserLang()
- //const { update } = useLang()
+ const { i18n } = useTranslationContext();
+ const borwserLang = getBrowserLang();
+ const { update } = useLang(undefined, {});
- const [value, updateValue] = useSettings()
- const errors: FormErrors<Settings> = {
- }
+ const [value, updateValue] = useSettings();
+ const errors: FormErrors<Settings> = {};
function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
- const next = s(value)
+ const next = s(value);
const v: Settings = {
advanceOrderMode: next.advanceOrderMode ?? false,
- dateFormat: next.dateFormat ?? "ymd"
- }
- updateValue(v)
+ 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>}
+ 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>
- </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>
+ </FormProvider>
+ </div>
</div>
+ <div class="column" />
</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
+ {onClose && (
+ <section class="section is-main-section">
+ <button class="button" onClick={onClose}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ )}
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/schemas/index.ts b/packages/auditor-backoffice-ui/src/schemas/index.ts
deleted file mode 100644
index 380466e13..000000000
--- a/packages/auditor-backoffice-ui/src/schemas/index.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- 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/_aside.scss b/packages/auditor-backoffice-ui/src/scss/_aside.scss
index e0922093b..719da7d2c 100644
--- a/packages/auditor-backoffice-ui/src/scss/_aside.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_aside.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -130,7 +130,11 @@ aside.aside {
@include touch {
nav.navbar {
- @include transition(margin-left);
+ // @include transition(margin-left);
+ // TODO: adapt above transition mixin to work with multiple transitions
+ transition:
+ margin-left 250ms ease-in-out 50ms,
+ width 250ms ease-in-out 50ms;
}
aside.aside {
@include transition(left);
@@ -173,6 +177,7 @@ aside.aside {
div.has-aside-mobile-expanded {
nav.navbar {
margin-left: $aside-mobile-width;
+ width: calc(100vw - $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
index 62db7f457..a4118400f 100644
--- a/packages/auditor-backoffice-ui/src/scss/_card.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_card.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
index 34c40092b..62414a00a 100644
--- a/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_custom-calendar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_footer.scss b/packages/auditor-backoffice-ui/src/scss/_footer.scss
index 5855af742..7e90c40cc 100644
--- a/packages/auditor-backoffice-ui/src/scss/_footer.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_footer.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_form.scss b/packages/auditor-backoffice-ui/src/scss/_form.scss
index bd28a17cf..126d3d0cc 100644
--- a/packages/auditor-backoffice-ui/src/scss/_form.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_form.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
index 0276468d7..cb3f438e9 100644
--- a/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_hero-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_loading.scss b/packages/auditor-backoffice-ui/src/scss/_loading.scss
index d88d8c355..32f64f276 100644
--- a/packages/auditor-backoffice-ui/src/scss/_loading.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_loading.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_main-section.scss b/packages/auditor-backoffice-ui/src/scss/_main-section.scss
index 5a8b20ba0..444af5235 100644
--- a/packages/auditor-backoffice-ui/src/scss/_main-section.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_main-section.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_misc.scss b/packages/auditor-backoffice-ui/src/scss/_misc.scss
index 045d087e2..a0dbc64fc 100644
--- a/packages/auditor-backoffice-ui/src/scss/_misc.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_misc.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_modal.scss b/packages/auditor-backoffice-ui/src/scss/_modal.scss
index b2bfd3e9e..d2565e7c7 100644
--- a/packages/auditor-backoffice-ui/src/scss/_modal.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_modal.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
index 406e0392f..4c0e2f5cc 100644
--- a/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_nav-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_table.scss b/packages/auditor-backoffice-ui/src/scss/_table.scss
index e4fbfc7b3..6c7765a74 100644
--- a/packages/auditor-backoffice-ui/src/scss/_table.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_table.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_theme-default.scss b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
index e74ece0e9..f34497bde 100644
--- a/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_theme-default.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_tiles.scss b/packages/auditor-backoffice-ui/src/scss/_tiles.scss
index 94dd6c21d..75bc6b94e 100644
--- a/packages/auditor-backoffice-ui/src/scss/_tiles.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_tiles.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/_title-bar.scss b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
index bac3f6b42..5de384a32 100644
--- a/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
+++ b/packages/auditor-backoffice-ui/src/scss/_title-bar.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
index a578506e8..591fc3da2 100644
--- a/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
+++ b/packages/auditor-backoffice-ui/src/scss/fonts/nunito.css
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/libs/_all.scss b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
index cba6f26eb..ab8030a13 100644
--- a/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
+++ b/packages/auditor-backoffice-ui/src/scss/libs/_all.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/scss/main.scss b/packages/auditor-backoffice-ui/src/scss/main.scss
index c4be8aa73..4a46472f9 100644
--- a/packages/auditor-backoffice-ui/src/scss/main.scss
+++ b/packages/auditor-backoffice-ui/src/scss/main.scss
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/stories.test.ts b/packages/auditor-backoffice-ui/src/stories.test.ts
deleted file mode 100644
index abd993550..000000000
--- a/packages/auditor-backoffice-ui/src/stories.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- 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
deleted file mode 100644
index 8bb06b8cb..000000000
--- a/packages/auditor-backoffice-ui/src/stories.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- 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/utils/amount.ts b/packages/auditor-backoffice-ui/src/utils/amount.ts
deleted file mode 100644
index 475489d3e..000000000
--- a/packages/auditor-backoffice-ui/src/utils/amount.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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
deleted file mode 100644
index 7c4e288b3..000000000
--- a/packages/auditor-backoffice-ui/src/utils/constants.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- 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/regex.test.ts b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
deleted file mode 100644
index 984f1a472..000000000
--- a/packages/auditor-backoffice-ui/src/utils/regex.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- 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
deleted file mode 100644
index db2b2021c..000000000
--- a/packages/auditor-backoffice-ui/src/utils/table.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- 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
index 0d249f3c4..de26cd82e 100644
--- a/packages/auditor-backoffice-ui/src/utils/types.ts
+++ b/packages/auditor-backoffice-ui/src/utils/types.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -14,12 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * Imports.
+ */
import { VNode } from "preact";
-export interface KeyValue {
- [key: string]: string;
-}
-
export interface Notification {
message: string;
description?: string | VNode;
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
index db89e58be..3ad474aca 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,11 +1,10 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.11.4",
+ "version": "0.13.4-dev.1",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
- "build": "./build.mjs",
"check": "tsc",
"clean": "rm -rf dist lib tsconfig.tsbuildinfo",
"compile": "tsc && ./build.mjs",
@@ -20,7 +19,6 @@
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
"date-fns": "2.29.3",
- "jed": "1.1.1",
"preact": "10.11.3",
"qrcode-generator": "^1.4.4",
"swr": "2.0.3"
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
index 380b267a2..0e4a264a8 100644
--- a/packages/bank-ui/src/Routing.tsx
+++ b/packages/bank-ui/src/Routing.tsx
@@ -31,7 +31,7 @@ import {
HttpStatusCode,
TranslatedString,
assertUnreachable,
- createRFC8959AccessTokenEncoded
+ createRFC8959AccessTokenEncoded,
} from "@gnu-taler/taler-util";
import { useEffect } from "preact/hooks";
import { useSessionState } from "./hooks/session.js";
@@ -108,10 +108,6 @@ function PublicRounting({
}
}, [location]);
- if (location === undefined) {
- return <Fragment />;
- }
-
async function doAutomaticLogin(username: string, password: string) {
await handleError(async () => {
const resp = await lib
@@ -122,14 +118,17 @@ function PublicRounting({
refreshable: true,
});
if (resp.type === "ok") {
- onLoggedUser(username, createRFC8959AccessTokenEncoded(resp.body.access_token));
+ onLoggedUser(
+ username,
+ createRFC8959AccessTokenEncoded(resp.body.access_token),
+ );
} else {
switch (resp.case) {
case HttpStatusCode.Unauthorized:
return notify({
type: "error",
title: i18n.str`Wrong credentials for "${username}"`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -137,7 +136,7 @@ function PublicRounting({
return notify({
type: "error",
title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -149,6 +148,7 @@ function PublicRounting({
}
switch (location.name) {
+ case undefined:
case "login": {
return (
<Fragment>
@@ -167,7 +167,7 @@ function PublicRounting({
<WithdrawalOperationPage
operationId={location.values.wopid}
routeWithdrawalDetails={publicPages.operationDetails}
- purpose="after-confirmation"
+ origin="from-wallet-ui"
onOperationAborted={() => navigateTo(publicPages.login.url({}))}
routeClose={publicPages.login}
onAuthorizationRequired={() =>
@@ -278,17 +278,13 @@ function PrivateRouting({
}
}, [location]);
- if (location === undefined) {
- return <Fragment />;
- }
-
switch (location.name) {
case "operationDetails": {
return (
<WithdrawalOperationPage
operationId={location.values.wopid}
routeWithdrawalDetails={privatePages.operationDetails}
- purpose="after-confirmation"
+ origin="from-wallet-ui"
onOperationAborted={() => navigateTo(privatePages.home.url({}))}
routeClose={privatePages.home}
onAuthorizationRequired={() =>
@@ -302,7 +298,7 @@ function PrivateRouting({
<WithdrawalOperationPage
operationId={location.values.wopid}
routeWithdrawalDetails={privatePages.operationDetails}
- purpose="after-creation"
+ origin="from-bank-ui"
onOperationAborted={() => navigateTo(privatePages.home.url({}))}
routeClose={privatePages.home}
onAuthorizationRequired={() =>
@@ -395,9 +391,7 @@ function PrivateRouting({
routeMyAccountDetails={privatePages.myAccountDetails}
routeMyAccountPassword={privatePages.myAccountPassword}
routeConversionConfig={privatePages.conversionConfig}
- onCashout={() =>
- navigateTo(privatePages.home.url({}))
- }
+ onCashout={() => navigateTo(privatePages.home.url({}))}
onAuthorizationRequired={() =>
navigateTo(privatePages.solveSecondFactor.url({}))
}
@@ -465,9 +459,7 @@ function PrivateRouting({
routeMyAccountDetails={privatePages.myAccountDetails}
routeMyAccountPassword={privatePages.myAccountPassword}
routeConversionConfig={privatePages.conversionConfig}
- onCashout={() =>
- navigateTo(privatePages.home.url({}))
- }
+ onCashout={() => navigateTo(privatePages.home.url({}))}
onAuthorizationRequired={() =>
navigateTo(privatePages.solveSecondFactor.url({}))
}
@@ -475,6 +467,7 @@ function PrivateRouting({
/>
);
}
+ case undefined:
case "home": {
if (isAdmin) {
return (
diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx
index 22b8d8c1b..bc9de3a60 100644
--- a/packages/bank-ui/src/components/Cashouts/views.tsx
+++ b/packages/bank-ui/src/components/Cashouts/views.tsx
@@ -24,6 +24,7 @@ import {
import {
Attention,
Loading,
+ Time,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
@@ -31,7 +32,6 @@ import { Fragment, VNode, h } from "preact";
import { useConversionInfo } from "../../hooks/regional.js";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
-import { Time } from "../Time.js";
import { State } from "./index.js";
export function FailedView({ error }: State.Failed) {
@@ -160,7 +160,6 @@ export function ReadyView({
timestamp={AbsoluteTime.fromProtocolTimestamp(
item.creation_time,
)}
- // relative={Duration.fromSpec({ days: 1 })}
/>
</div>
{
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx
index 10d63e6af..038603f9e 100644
--- a/packages/bank-ui/src/components/Transactions/views.tsx
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -16,13 +16,13 @@
import {
Attention,
+ Time,
useBankCoreApiContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
-import { Time } from "../Time.js";
import { State } from "./index.js";
export function ReadyView({
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
index 43d43a3f2..e9f2896c8 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -72,7 +72,7 @@ export function revalidateWithdrawalDetails() {
);
}
-export function useWithdrawalDetails(wid: string) {
+export function useWithdrawalDetails(wid: string | undefined) {
const {
lib: { bank: api },
} = useBankCoreApiContext();
@@ -91,7 +91,7 @@ export function useWithdrawalDetails(wid: string) {
const { data, error } = useSWR<
TalerCoreBankResultByMethod<"getWithdrawalById">,
TalerHttpError
- >([wid, latestStatus, "getWithdrawalById"], fetcher, {
+ >(wid === undefined ? undefined : [wid, latestStatus, "getWithdrawalById"], fetcher, {
refreshInterval: 3000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -292,7 +292,7 @@ export function useTransactions(account: string, initial?: number) {
TalerCoreBankResultByMethod<"getTransactions">,
TalerHttpError
>([account, token, offset, "getTransactions"], fetcher, {
- refreshInterval: 0,
+ refreshInterval: 10000,
refreshWhenHidden: false,
refreshWhenOffline: false,
// revalidateOnMount: false,
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index 9c60456c7..a03234634 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -19,7 +19,6 @@ import {
TranslatedString,
buildCodecForObject,
codecForBoolean,
- codecForNumber,
} from "@gnu-taler/taler-util";
import {
buildStorageKey,
@@ -31,9 +30,9 @@ interface Preferences {
showWithdrawalSuccess: boolean;
showDemoDescription: boolean;
showInstallWallet: boolean;
- maxWithdrawalAmount: number;
- fastWithdrawal: boolean;
showDebugInfo: boolean;
+ fastWithdrawalForm: boolean;
+ showCopyAccount: boolean;
}
export const codecForPreferences = (): Codec<Preferences> =>
@@ -41,18 +40,18 @@ export const codecForPreferences = (): Codec<Preferences> =>
.property("showWithdrawalSuccess", codecForBoolean())
.property("showDemoDescription", codecForBoolean())
.property("showInstallWallet", codecForBoolean())
- .property("fastWithdrawal", codecForBoolean())
.property("showDebugInfo", codecForBoolean())
- .property("maxWithdrawalAmount", codecForNumber())
- .build("Settings");
+ .property("fastWithdrawalForm", codecForBoolean())
+ .property("showCopyAccount", codecForBoolean())
+ .build("Preferences");
const defaultPreferences: Preferences = {
showWithdrawalSuccess: true,
showDemoDescription: true,
showInstallWallet: true,
- maxWithdrawalAmount: 25,
- fastWithdrawal: false,
showDebugInfo: false,
+ fastWithdrawalForm: false,
+ showCopyAccount: false,
};
const BANK_PREFERENCES_KEY = buildStorageKey(
@@ -82,11 +81,12 @@ export function usePreferences(): [
export function getAllBooleanPreferences(): Array<keyof Preferences> {
return [
- "fastWithdrawal",
"showDebugInfo",
"showDemoDescription",
"showInstallWallet",
"showWithdrawalSuccess",
+ "fastWithdrawalForm",
+ "showCopyAccount",
];
}
@@ -95,16 +95,16 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
- case "maxWithdrawalAmount":
- return i18n.str`Max withdrawal amount`;
case "showWithdrawalSuccess":
return i18n.str`Show withdrawal confirmation`;
+ case "fastWithdrawalForm":
+ return i18n.str`Withdraw without setting amount`;
+ case "showCopyAccount":
+ return i18n.str`Show copy account letter`;
case "showDemoDescription":
return i18n.str`Show demo description`;
case "showInstallWallet":
return i18n.str`Show install wallet first`;
- case "fastWithdrawal":
- return i18n.str`Set the withdrawal amount in the wallet`;
case "showDebugInfo":
return i18n.str`Show debug info`;
}
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
index e0c861a0f..417874cf8 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -42,10 +42,10 @@ const useSWR = _useSWR as unknown as SWRHook;
export type TransferCalculation =
| {
- debit: AmountJson;
- credit: AmountJson;
- beforeFee: AmountJson;
- }
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
| "amount-is-too-small";
type EstimatorFunction = (
amount: AmountJson,
@@ -109,7 +109,11 @@ export function useCashinEstimator(): ConversionEstimators {
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
case HttpStatusCode.BadRequest: //we are using just one parameter
- throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get conversion cashin rate"))
+ }
}
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
@@ -134,7 +138,11 @@ export function useCashinEstimator(): ConversionEstimators {
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
case HttpStatusCode.BadRequest: //we are using just one parameter
- throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get conversion cashin rate"))
+ }
}
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
@@ -167,7 +175,11 @@ export function useCashoutEstimator(): ConversionEstimators {
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
case HttpStatusCode.BadRequest: //we are using just one parameter
- throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get conversion cashout rate"))
+ }
}
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
@@ -192,7 +204,11 @@ export function useCashoutEstimator(): ConversionEstimators {
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
case HttpStatusCode.BadRequest: //we are using just one parameter
- throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get conversion cashout rate"))
+ }
}
}
const credit = Amounts.parseOrThrow(resp.body.amount_credit);
diff --git a/packages/bank-ui/src/i18n/de.po b/packages/bank-ui/src/i18n/de.po
index 54fda4377..d01d38aa6 100644
--- a/packages/bank-ui/src/i18n/de.po
+++ b/packages/bank-ui/src/i18n/de.po
@@ -14,8 +14,8 @@ 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: 2024-05-05 09:32+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-09-26 07:20+0000\n"
+"Last-Translator: Stefan Kügel <stefan.kuegel@taler.net>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/de/>\n"
"Language: de\n"
@@ -23,12 +23,12 @@ msgstr ""
"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.4.3\n"
+"X-Generator: Weblate 5.5.5\n"
#: src/utils.ts:137
#, c-format
msgid "Operation failed, please report"
-msgstr "Vorgang abgebrochen, bitte Fehler berichten"
+msgstr "Vorgang fehlgeschlagen, bitte Fehler melden"
#: src/utils.ts:156
#, c-format
@@ -63,12 +63,12 @@ msgstr "Unerwarteter Fehler"
#: src/utils.ts:377
#, c-format
msgid "IBAN numbers usually have more that 4 digits"
-msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
+msgstr "Eine IBAN besteht normalerweise aus mehr als 4 Ziffern"
#: src/utils.ts:379
#, c-format
msgid "IBAN numbers usually have less that 34 digits"
-msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
+msgstr "Eine IBAN besteht normalerweise aus weniger als 34 Ziffern"
#: src/utils.ts:387
#, c-format
@@ -97,12 +97,12 @@ msgstr "Höchste Abhebesumme"
#: src/hooks/preferences.ts:57
#, c-format
msgid "Show withdrawal confirmation"
-msgstr ""
+msgstr "Zeige Bestätigung der Abhebung"
#: src/hooks/preferences.ts:59
#, c-format
msgid "Show demo description"
-msgstr ""
+msgstr "Demobeschreibung anzeigen"
#: src/hooks/preferences.ts:61
#, c-format
@@ -112,133 +112,136 @@ msgstr ""
#: src/hooks/preferences.ts:63
#, c-format
msgid "Use fast withdrawal form"
-msgstr ""
+msgstr "Nutze schnelles Abhebungsformular"
#: src/hooks/preferences.ts:65
#, c-format
msgid "Show debug info"
-msgstr ""
+msgstr "Debug-Informationen anzeigen"
#: src/pages/PaytoWireTransferForm.tsx:90
#, c-format
msgid "required"
-msgstr ""
+msgstr "erforderlich"
#: src/pages/PaytoWireTransferForm.tsx:92
#, c-format
msgid "IBAN should have just uppercased letters and numbers"
-msgstr ""
+msgstr "IBAN sollte nur Großbuchstaben und Zahlen enthalten"
#: src/pages/PaytoWireTransferForm.tsx:98
#, c-format
msgid "not valid"
-msgstr ""
+msgstr "nicht gültig"
#: src/pages/PaytoWireTransferForm.tsx:100
#, c-format
msgid "should be greater than 0"
-msgstr ""
+msgstr "sollte größer als 0 sein"
#: src/pages/PaytoWireTransferForm.tsx:102
#, c-format
msgid "balance is not enough"
-msgstr ""
+msgstr "Der Saldo ist nicht ausreichend"
#: src/pages/PaytoWireTransferForm.tsx:112
#, c-format
msgid "does not follow the pattern"
-msgstr ""
+msgstr "Weicht vom Muster ab"
#: src/pages/PaytoWireTransferForm.tsx:114
#, c-format
msgid "only \"IBAN\" target are supported"
-msgstr ""
+msgstr "Nur \"IBAN\" Ziele werden unterstützt"
#: src/pages/PaytoWireTransferForm.tsx:116
#, c-format
msgid "use the \"amount\" parameter to specify the amount to be transferred"
-msgstr ""
+msgstr "Nutze den Parameter \"amount\" um den zu sendenden Betrag anzugeben"
#: src/pages/PaytoWireTransferForm.tsx:118
#, c-format
msgid "the amount is not valid"
-msgstr ""
+msgstr "der Betrag ist nicht gültig"
#: src/pages/PaytoWireTransferForm.tsx:120
#, c-format
msgid ""
"use the \"message\" parameter to specify a reference text for the transfer"
msgstr ""
+"Nutze den Parameter \"message\", um einen Referenztext für den Transfer "
+"anzugeben"
#: src/pages/PaytoWireTransferForm.tsx:160
#, c-format
msgid "The request was invalid or the payto://-URI used unacceptable features."
msgstr ""
+"Die Anfrage war ungültig oder die payto://-URI nutzte inakzeptable Merkmale."
#: src/pages/PaytoWireTransferForm.tsx:167
#, c-format
msgid "Not enough permission to complete the operation."
-msgstr ""
+msgstr "Nicht genug Berechtigungen, um den Vorgang abzuschließen."
#: src/pages/PaytoWireTransferForm.tsx:174
#, c-format
msgid "The destination account \"%1$s\" was not found."
-msgstr ""
+msgstr "Das Zielkonto \"%1$s\" wurde nicht gefunden."
#: src/pages/PaytoWireTransferForm.tsx:181
#, c-format
msgid "The origin and the destination of the transfer can't be the same."
-msgstr ""
+msgstr "Ursprung und Ziel des Transfers können nicht gleich sein."
#: src/pages/PaytoWireTransferForm.tsx:188
#, c-format
msgid "Your balance is not enough."
-msgstr ""
+msgstr "Der Saldo ist nicht ausreichend."
#: src/pages/PaytoWireTransferForm.tsx:195
#, c-format
msgid "The origin account \"%1$s\" was not found."
-msgstr ""
+msgstr "Das Ursprungskonto \"%1$s\" wurde nicht gefunden."
#: src/pages/PaytoWireTransferForm.tsx:212
#, c-format
msgid "Wire transfer created!"
-msgstr ""
+msgstr "Banküberweisung erstellt!"
#: src/pages/PaytoWireTransferForm.tsx:270
#, c-format
msgid "Using a form"
-msgstr ""
+msgstr "Mithilfe eines Formulars"
#: src/pages/PaytoWireTransferForm.tsx:310
#, c-format
msgid "Import payto:// URI"
-msgstr ""
+msgstr "Importiere payto:// URI"
#: src/pages/PaytoWireTransferForm.tsx:335
#, c-format
msgid "Recipient"
-msgstr ""
+msgstr "Empfänger"
#: src/pages/PaytoWireTransferForm.tsx:359
#, c-format
msgid "IBAN of the recipient's account"
-msgstr ""
+msgstr "IBAN des Empfängerkontos"
#: src/pages/PaytoWireTransferForm.tsx:369
#, c-format
msgid "Transfer subject"
-msgstr ""
+msgstr "Buchungsvermerk der Überweisung"
#: src/pages/PaytoWireTransferForm.tsx:377
-#, fuzzy, c-format
+#, c-format
msgid "subject"
-msgstr "Verwendungszweck"
+msgstr "Buchungsvermerk"
#: src/pages/PaytoWireTransferForm.tsx:390
#, c-format
msgid "some text to identify the transfer"
-msgstr ""
+msgstr "Etwas Text, um den Transfer zu identifizieren"
#: src/pages/PaytoWireTransferForm.tsx:400
#, c-format
@@ -246,14 +249,14 @@ msgid "Amount"
msgstr "Betrag"
#: src/pages/PaytoWireTransferForm.tsx:415
-#, fuzzy, c-format
+#, c-format
msgid "amount to transfer"
-msgstr "Betrag"
+msgstr "Zu sendender Betrag"
#: src/pages/PaytoWireTransferForm.tsx:425
#, c-format
msgid "payto URI:"
-msgstr ""
+msgstr "payto URI:"
#: src/pages/PaytoWireTransferForm.tsx:436
#, c-format
@@ -264,6 +267,7 @@ msgstr ""
#, c-format
msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
msgstr ""
+"payto://iban/[receiver-iban]?message=[Buchungsvermerk]&amount=[%1$s:X.Y]"
#: src/pages/PaytoWireTransferForm.tsx:457
#, c-format
@@ -278,62 +282,62 @@ msgstr ""
#: src/pages/LoginForm.tsx:71
#, c-format
msgid "Missing username"
-msgstr ""
+msgstr "Fehlender Benutzername"
#: src/pages/LoginForm.tsx:75
#, c-format
msgid "Missing password"
-msgstr ""
+msgstr "Fehlendes Passwort"
#: src/pages/LoginForm.tsx:104
#, c-format
msgid "Wrong credentials for \"%1$s\""
-msgstr ""
+msgstr "Falsche Zugangsdaten für \"%1$s\""
#: src/pages/LoginForm.tsx:111
#, c-format
msgid "Account not found"
-msgstr ""
+msgstr "Konto nicht gefunden"
#: src/pages/LoginForm.tsx:142
#, c-format
msgid "Username"
-msgstr ""
+msgstr "Benutzername"
#: src/pages/LoginForm.tsx:156
#, c-format
msgid "username of the account"
-msgstr ""
+msgstr "Benutzername des Kontos"
#: src/pages/LoginForm.tsx:175
#, c-format
msgid "Password"
-msgstr ""
+msgstr "Passwort"
#: src/pages/LoginForm.tsx:188
-#, fuzzy, c-format
+#, c-format
msgid "password of the account"
-msgstr "Buchungen auf öffentlich sichtbaren Konten"
+msgstr "Passwort des Kontos"
#: src/pages/LoginForm.tsx:223
#, c-format
msgid "Check"
-msgstr ""
+msgstr "Überprüfung"
#: src/pages/LoginForm.tsx:237
#, c-format
msgid "Log in"
-msgstr ""
+msgstr "Anmelden"
#: src/pages/LoginForm.tsx:249
#, c-format
msgid "Register"
-msgstr ""
+msgstr "Registrieren"
#: src/components/Transactions/views.tsx:52
#, c-format
msgid "Latest transactions"
-msgstr ""
+msgstr "Neueste Transaktionen"
#: src/components/Transactions/views.tsx:63
#, c-format
@@ -343,52 +347,52 @@ msgstr "Datum"
#: src/components/Transactions/views.tsx:71
#, c-format
msgid "Counterpart"
-msgstr "Empfänger"
+msgstr "Gegenkonto"
#: src/components/Transactions/views.tsx:75
#, c-format
msgid "Subject"
-msgstr "Verwendungszweck"
+msgstr "Buchungsvermerk"
#: src/components/Transactions/views.tsx:111
#, c-format
msgid "sent"
-msgstr ""
+msgstr "gesendet"
#: src/components/Transactions/views.tsx:112
#, c-format
msgid "received"
-msgstr ""
+msgstr "empfangen"
#: src/components/Transactions/views.tsx:127
#, c-format
msgid "invalid value"
-msgstr ""
+msgstr "ungültiger Wert"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "to"
-msgstr ""
+msgstr "an"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "from"
-msgstr ""
+msgstr "von"
#: src/components/Transactions/views.tsx:202
#, c-format
msgid "First page"
-msgstr ""
+msgstr "Erste Seite"
#: src/components/Transactions/views.tsx:209
#, c-format
msgid "Next"
-msgstr ""
+msgstr "Nächste"
#: src/pages/WithdrawalConfirmationQuestion.tsx:86
#, c-format
msgid "Wire transfer completed!"
-msgstr ""
+msgstr "Banküberweisung abgeschlossen!"
#: src/pages/WithdrawalConfirmationQuestion.tsx:93
#, c-format
@@ -424,7 +428,7 @@ msgid ""
msgstr ""
#: src/pages/WithdrawalConfirmationQuestion.tsx:186
-#, fuzzy, c-format
+#, c-format
msgid "Confirm the withdrawal operation"
msgstr "Abhebung bestätigen"
@@ -526,9 +530,9 @@ msgid "There is an operation already"
msgstr ""
#: src/pages/WalletWithdrawForm.tsx:75
-#, fuzzy, c-format
+#, c-format
msgid "Complete or cancel the operation in"
-msgstr "Abhebung bestätigen"
+msgstr "Abschließen oder Abbruch in"
#: src/pages/WalletWithdrawForm.tsx:84
#, c-format
@@ -693,9 +697,9 @@ msgid "Make a wire transfer"
msgstr ""
#: src/pages/admin/AccountList.tsx:72
-#, fuzzy, c-format
+#, c-format
msgid "Accounts"
-msgstr "Betrag"
+msgstr "Konten"
#: src/pages/admin/AccountList.tsx:75
#, c-format
@@ -710,12 +714,12 @@ msgstr ""
#: src/pages/admin/AccountList.tsx:106
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Name"
#: src/pages/admin/AccountList.tsx:110
#, c-format
msgid "Balance"
-msgstr ""
+msgstr "Saldo"
#: src/pages/admin/AccountList.tsx:112
#, c-format
@@ -983,7 +987,7 @@ msgstr ""
#: src/pages/QrCodeSection.tsx:143
#, c-format
msgid "Withdraw"
-msgstr ""
+msgstr "Abheben"
#: src/pages/QrCodeSection.tsx:152
#, c-format
@@ -1122,17 +1126,17 @@ msgstr ""
#: src/pages/SolveChallengePage.tsx:230
#, c-format
msgid "Wire transfer"
-msgstr ""
+msgstr "Banküberweisung"
#: src/pages/SolveChallengePage.tsx:232
-#, fuzzy, c-format
+#, c-format
msgid "Withdrawal"
-msgstr "Abhebung bestätigen"
+msgstr "Abhebung"
#: src/pages/SolveChallengePage.tsx:248
-#, fuzzy, c-format
+#, c-format
msgid "Confirm the operation"
-msgstr "Abhebung bestätigen"
+msgstr "Vorgang bestätigen"
#: src/pages/SolveChallengePage.tsx:271
#, c-format
@@ -1212,7 +1216,7 @@ msgstr ""
#: src/pages/ProfileNavigation.tsx:74
#, c-format
msgid "Delete"
-msgstr ""
+msgstr "Löschen"
#: src/pages/ProfileNavigation.tsx:78
#, c-format
@@ -1308,9 +1312,9 @@ msgid "Before doing a cashout you need to complete your profile"
msgstr ""
#: src/pages/business/CreateCashout.tsx:440
-#, fuzzy, c-format
+#, c-format
msgid "Amount to send"
-msgstr "Betrag"
+msgstr "Zu sendender Betrag"
#: src/pages/business/CreateCashout.tsx:441
#, c-format
diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po
index 39527f1dd..fdfda5638 100644
--- a/packages/bank-ui/src/i18n/es.po
+++ b/packages/bank-ui/src/i18n/es.po
@@ -14,8 +14,8 @@ 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: 2024-02-13 14:40+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-06-26 08:05+0000\n"
+"Last-Translator: Luis Avalos <avalos.diaz.0577@gmail.com>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/es/>\n"
"Language: es\n"
@@ -23,7 +23,7 @@ msgstr ""
"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"
+"X-Generator: Weblate 5.5.5\n"
#: src/utils.ts:137
#, c-format
@@ -614,7 +614,7 @@ msgstr "a una billetera %1$s"
#: src/pages/PaymentOptions.tsx:95
#, c-format
msgid "Withdraw digital money into your mobile wallet or browser extension"
-msgstr "Extraer dinero digital a tu billetera móvil o extesión web"
+msgstr "Extraer dinero digital a tu billetera móvil o extensión web"
#: src/pages/PaymentOptions.tsx:109
#, c-format
diff --git a/packages/bank-ui/src/i18n/uk.po b/packages/bank-ui/src/i18n/uk.po
index a8b41e32f..35b0386df 100644
--- a/packages/bank-ui/src/i18n/uk.po
+++ b/packages/bank-ui/src/i18n/uk.po
@@ -16,8 +16,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Taler Bank\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
-"PO-Revision-Date: 2024-03-07 07:04+0000\n"
-"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"PO-Revision-Date: 2024-08-07 10:40+0000\n"
+"Last-Translator: Vlada Svirsh <vlada.svirsh@students.bfh.ch>\n"
"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/uk/>\n"
"Language: uk\n"
@@ -26,12 +26,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.5.5\n"
#: src/utils.ts:137
-#, c-format, fuzzy
+#, c-format
msgid "Operation failed, please report"
-msgstr "Помилка операції, повідомте"
+msgstr "Операція не вдалася, будь ласка, повідомте про це"
#: src/utils.ts:156
#, c-format
@@ -39,9 +39,9 @@ msgid "Request timeout"
msgstr "Тайм-аут запиту"
#: src/utils.ts:165
-#, c-format, fuzzy
+#, c-format
msgid "Request throttled"
-msgstr "Запит до сервера перервано"
+msgstr "Запит затримується"
#: src/utils.ts:174
#, c-format
@@ -64,24 +64,24 @@ msgid "Unexpected error"
msgstr "Неочікувана помилка"
#: src/utils.ts:377
-#, c-format, fuzzy
+#, c-format
msgid "IBAN numbers usually have more that 4 digits"
-msgstr "Номера IBAN зазвичай мають більше 4ьох цифр"
+msgstr "Номера IBAN зазвичай мають більше 4-ьох цифр"
#: src/utils.ts:379
-#, c-format, fuzzy
+#, c-format
msgid "IBAN numbers usually have less that 34 digits"
-msgstr "Номера IBAN зазвичай мають менше 34ьох цифр"
+msgstr "Номера IBAN зазвичай мають менше 34-ьох цифр"
#: src/utils.ts:387
-#, c-format, fuzzy
+#, c-format
msgid "IBAN country code not found"
msgstr "Код країни IBAN не знайдено"
#: src/utils.ts:401
-#, c-format, fuzzy
+#, c-format
msgid "IBAN number is not valid, checksum is wrong"
-msgstr "Номер IBAN невірний, контрольна сума не сходиться"
+msgstr "Номер IBAN не коректний, контрольна сума не сходиться"
#: src/context/config.ts:136
#, c-format
@@ -89,16 +89,18 @@ msgid ""
"the bank backend is not supported. supported version \"%1$s\", server version "
"\"%2$s\""
msgstr ""
+"бекенд банку не підтримується. підтримувана версія: \"%1$s\", версія сервера:"
+" \"%2$s\""
#: src/hooks/preferences.ts:55
#, c-format
msgid "Max withdrawal amount"
-msgstr "Максимальна сумма для виведення"
+msgstr "Максимальна сума зняття"
#: src/hooks/preferences.ts:57
#, c-format
msgid "Show withdrawal confirmation"
-msgstr "Показати підтвердження виводу"
+msgstr "Показати підтвердження зняття коштів"
#: src/hooks/preferences.ts:59
#, c-format
@@ -108,162 +110,166 @@ msgstr "Показати демо опис"
#: src/hooks/preferences.ts:61
#, c-format
msgid "Show install wallet first"
-msgstr ""
+msgstr "Спочатку показати, як встановити гаманець"
#: src/hooks/preferences.ts:63
#, c-format
msgid "Use fast withdrawal form"
-msgstr ""
+msgstr "Використовуйте форму швидкого зняття коштів"
#: src/hooks/preferences.ts:65
#, c-format
msgid "Show debug info"
-msgstr ""
+msgstr "Показати інформацію для відладки"
#: src/pages/PaytoWireTransferForm.tsx:90
#, c-format
msgid "required"
-msgstr ""
+msgstr "обовʼязково"
#: src/pages/PaytoWireTransferForm.tsx:92
#, c-format
msgid "IBAN should have just uppercased letters and numbers"
-msgstr ""
+msgstr "IBAN повинен містити лише великі літери та цифри"
#: src/pages/PaytoWireTransferForm.tsx:98
#, c-format
msgid "not valid"
-msgstr ""
+msgstr "недійсний"
#: src/pages/PaytoWireTransferForm.tsx:100
#, c-format
msgid "should be greater than 0"
-msgstr ""
+msgstr "має бути більшим за 0"
#: src/pages/PaytoWireTransferForm.tsx:102
#, c-format
msgid "balance is not enough"
-msgstr ""
+msgstr "недостатній баланс"
#: src/pages/PaytoWireTransferForm.tsx:112
#, c-format
msgid "does not follow the pattern"
-msgstr ""
+msgstr "не відповідає шаблону"
#: src/pages/PaytoWireTransferForm.tsx:114
#, c-format
msgid "only \"IBAN\" target are supported"
-msgstr ""
+msgstr "підтримуються лише цілі \"IBAN\""
#: src/pages/PaytoWireTransferForm.tsx:116
#, c-format
msgid "use the \"amount\" parameter to specify the amount to be transferred"
-msgstr ""
+msgstr "використовуйте параметр \"amount\", щоб вказати суму для переказу"
#: src/pages/PaytoWireTransferForm.tsx:118
#, c-format
msgid "the amount is not valid"
-msgstr ""
+msgstr "сума недійсна"
#: src/pages/PaytoWireTransferForm.tsx:120
#, c-format
msgid "use the \"message\" parameter to specify a reference text for the transfer"
msgstr ""
+"використовуйте параметр \"message\", щоб вказати довідковий текст для "
+"переказу"
#: src/pages/PaytoWireTransferForm.tsx:160
#, c-format
msgid "The request was invalid or the payto://-URI used unacceptable features."
-msgstr ""
+msgstr "Запит недійсний або payto://-URI використовує неприпустимі функції."
#: src/pages/PaytoWireTransferForm.tsx:167
#, c-format
msgid "Not enough permission to complete the operation."
-msgstr ""
+msgstr "Недостатньо прав для виконання операції."
#: src/pages/PaytoWireTransferForm.tsx:174
#, c-format
msgid "The destination account \"%1$s\" was not found."
-msgstr ""
+msgstr "Обліковий запис призначення \"%1$s\" не знайдено."
#: src/pages/PaytoWireTransferForm.tsx:181
#, c-format
msgid "The origin and the destination of the transfer can't be the same."
-msgstr ""
+msgstr "Джерело та місце призначення переказу не можуть бути однаковими."
#: src/pages/PaytoWireTransferForm.tsx:188
#, c-format
msgid "Your balance is not enough."
-msgstr ""
+msgstr "Ваш баланс недостатній."
#: src/pages/PaytoWireTransferForm.tsx:195
#, c-format
msgid "The origin account \"%1$s\" was not found."
-msgstr ""
+msgstr "Обліковий запис джерела \"%1$s\" не знайдено."
#: src/pages/PaytoWireTransferForm.tsx:212
#, c-format
msgid "Wire transfer created!"
-msgstr ""
+msgstr "Банківський переказ створено!"
#: src/pages/PaytoWireTransferForm.tsx:270
#, c-format
msgid "Using a form"
-msgstr ""
+msgstr "Використовуючи форму"
#: src/pages/PaytoWireTransferForm.tsx:310
#, c-format
msgid "Import payto:// URI"
-msgstr ""
+msgstr "Імпорт payto:// URI"
#: src/pages/PaytoWireTransferForm.tsx:335
#, c-format
msgid "Recipient"
-msgstr ""
+msgstr "Одержувач"
#: src/pages/PaytoWireTransferForm.tsx:359
#, c-format
msgid "IBAN of the recipient's account"
-msgstr ""
+msgstr "IBAN рахунку одержувача"
#: src/pages/PaytoWireTransferForm.tsx:369
#, c-format
msgid "Transfer subject"
-msgstr ""
+msgstr "Призначення переказу"
#: src/pages/PaytoWireTransferForm.tsx:377
#, c-format
msgid "subject"
-msgstr ""
+msgstr "призначення"
#: src/pages/PaytoWireTransferForm.tsx:390
#, c-format
msgid "some text to identify the transfer"
-msgstr ""
+msgstr "текст для ідентифікації переказу"
#: src/pages/PaytoWireTransferForm.tsx:400
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Сума"
#: src/pages/PaytoWireTransferForm.tsx:415
#, c-format
msgid "amount to transfer"
-msgstr ""
+msgstr "сума для переказу"
#: src/pages/PaytoWireTransferForm.tsx:425
#, c-format
msgid "payto URI:"
-msgstr ""
+msgstr "payto URI:"
#: src/pages/PaytoWireTransferForm.tsx:436
#, c-format
msgid "uniform resource identifier of the target account"
-msgstr ""
+msgstr "уніфікований ідентифікатор ресурсу цільового рахунку"
#: src/pages/PaytoWireTransferForm.tsx:437
#, c-format
msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
msgstr ""
+"payto://iban/[iban-отримувача]?message=[призначення-платежу]&amount=[%1$s:X."
+"Y]"
#: src/pages/PaytoWireTransferForm.tsx:457
#, c-format
@@ -278,122 +284,122 @@ msgstr ""
#: src/pages/LoginForm.tsx:71
#, c-format
msgid "Missing username"
-msgstr ""
+msgstr "Відсутнє ім'я користувача"
#: src/pages/LoginForm.tsx:75
#, c-format
msgid "Missing password"
-msgstr ""
+msgstr "Відсутній пароль"
#: src/pages/LoginForm.tsx:104
#, c-format
msgid "Wrong credentials for \"%1$s\""
-msgstr ""
+msgstr "Неправильні облікові дані для \"%1$s\""
#: src/pages/LoginForm.tsx:111
#, c-format
msgid "Account not found"
-msgstr ""
+msgstr "Обліковий запис не знайдено"
#: src/pages/LoginForm.tsx:142
#, c-format
msgid "Username"
-msgstr ""
+msgstr "Імʼя користувача"
#: src/pages/LoginForm.tsx:156
#, c-format
msgid "username of the account"
-msgstr ""
+msgstr "ім'я користувача облікового запису"
#: src/pages/LoginForm.tsx:175
#, c-format
msgid "Password"
-msgstr ""
+msgstr "Пароль"
#: src/pages/LoginForm.tsx:188
#, c-format
msgid "password of the account"
-msgstr ""
+msgstr "пароль облікового запису"
#: src/pages/LoginForm.tsx:223
#, c-format
msgid "Check"
-msgstr ""
+msgstr "Перевірити"
#: src/pages/LoginForm.tsx:237
#, c-format
msgid "Log in"
-msgstr ""
+msgstr "Увійти"
#: src/pages/LoginForm.tsx:249
#, c-format
msgid "Register"
-msgstr ""
+msgstr "Реєстрація"
#: src/components/Transactions/views.tsx:52
#, c-format
msgid "Latest transactions"
-msgstr ""
+msgstr "Останні транзакції"
#: src/components/Transactions/views.tsx:63
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Дата"
#: src/components/Transactions/views.tsx:71
#, c-format
msgid "Counterpart"
-msgstr ""
+msgstr "Контрагент"
#: src/components/Transactions/views.tsx:75
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Призначення"
#: src/components/Transactions/views.tsx:111
#, c-format
msgid "sent"
-msgstr ""
+msgstr "відправлено"
#: src/components/Transactions/views.tsx:112
#, c-format
msgid "received"
-msgstr ""
+msgstr "отримано"
#: src/components/Transactions/views.tsx:127
#, c-format
msgid "invalid value"
-msgstr ""
+msgstr "недійсне значення"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "to"
-msgstr ""
+msgstr "до"
#: src/components/Transactions/views.tsx:136
#, c-format
msgid "from"
-msgstr ""
+msgstr "від"
#: src/components/Transactions/views.tsx:202
#, c-format
msgid "First page"
-msgstr ""
+msgstr "Перша сторінка"
#: src/components/Transactions/views.tsx:209
#, c-format
msgid "Next"
-msgstr ""
+msgstr "Далі"
#: src/pages/WithdrawalConfirmationQuestion.tsx:86
#, c-format
msgid "Wire transfer completed!"
-msgstr ""
+msgstr "Банківський переказ завершено!"
#: src/pages/WithdrawalConfirmationQuestion.tsx:93
#, c-format
msgid "The withdrawal has been aborted previously and can't be confirmed"
-msgstr ""
+msgstr "Виведення коштів було скасовано раніше і не може бути підтверджено"
#: src/pages/WithdrawalConfirmationQuestion.tsx:100
#, c-format
@@ -401,61 +407,63 @@ msgid ""
"The withdrawal operation can't be confirmed before a wallet accepted the "
"transaction."
msgstr ""
+"Операцію зняття коштів не можна підтвердити, доки гаманець не прийме "
+"транзакцію."
#: src/pages/WithdrawalConfirmationQuestion.tsx:107
#, c-format
msgid "The operation id is invalid."
-msgstr ""
+msgstr "Ідентифікатор операції недійсний."
#: src/pages/WithdrawalConfirmationQuestion.tsx:114
#, c-format
msgid "The operation was not found."
-msgstr ""
+msgstr "Операцію не знайдено."
#: src/pages/WithdrawalConfirmationQuestion.tsx:121
#, c-format
msgid "Your balance is not enough for the operation."
-msgstr ""
+msgstr "Ваш баланс недостатній для виконання операції."
#: src/pages/WithdrawalConfirmationQuestion.tsx:155
#, c-format
msgid "The reserve operation has been confirmed previously and can't be aborted"
-msgstr ""
+msgstr "Операція резервування була підтверджена раніше і не може бути скасована"
#: src/pages/WithdrawalConfirmationQuestion.tsx:186
#, c-format
msgid "Confirm the withdrawal operation"
-msgstr ""
+msgstr "Підтвердити операцію зняття коштів"
#: src/pages/WithdrawalConfirmationQuestion.tsx:203
#, c-format
msgid "Wire transfer details"
-msgstr ""
+msgstr "Деталі банківського переказу"
#: src/pages/WithdrawalConfirmationQuestion.tsx:217
#, c-format
msgid "Taler Exchange operator's account"
-msgstr ""
+msgstr "Обліковий запис оператора обмінного пункту Taler"
#: src/pages/WithdrawalConfirmationQuestion.tsx:228
#, c-format
msgid "Taler Exchange operator's name"
-msgstr ""
+msgstr "Ім'я оператора обмінного пункту Taler"
#: src/pages/WithdrawalConfirmationQuestion.tsx:317
#, c-format
msgid "Transfer"
-msgstr ""
+msgstr "Переказати"
#: src/pages/WithdrawalConfirmationQuestion.tsx:342
#, c-format
msgid "Authentication required"
-msgstr ""
+msgstr "Потрібна автентифікація"
#: src/pages/WithdrawalConfirmationQuestion.tsx:352
#, c-format
msgid "This operation was created with other username"
-msgstr ""
+msgstr "Ця операція була створена з іншим іменем користувача"
#: src/pages/OperationState/views.tsx:209
#, c-format
@@ -463,16 +471,18 @@ msgid ""
"Unauthorized to make the operation, maybe the session has expired or the "
"password changed."
msgstr ""
+"Не авторизовано для виконання операції, можливо, сесія закінчилася або "
+"пароль було змінено."
#: src/pages/OperationState/views.tsx:218
#, c-format
msgid "The operation was rejected due to insufficient funds."
-msgstr ""
+msgstr "Операцію було відхилено через недостатність коштів."
#: src/pages/OperationState/views.tsx:268
#, c-format
msgid "Withdrawal confirmed"
-msgstr ""
+msgstr "Зняття коштів підтверджено"
#: src/pages/OperationState/views.tsx:272
#, c-format
@@ -480,21 +490,23 @@ msgid ""
"The wire transfer to the Taler operator has been initiated. You will soon "
"receive the requested amount in your Taler wallet."
msgstr ""
+"Банківський переказ до оператора Taler було ініційовано. Незабаром ви "
+"отримаєте запитану суму у ваш гаманець Taler."
#: src/pages/OperationState/views.tsx:287
#, c-format
msgid "Do not show this again"
-msgstr ""
+msgstr "Більше не показувати це"
#: src/pages/OperationState/views.tsx:319
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Закрити"
#: src/pages/OperationState/views.tsx:399
#, c-format
msgid "On this device"
-msgstr ""
+msgstr "На цьому пристрої"
#: src/pages/OperationState/views.tsx:404
#, c-format
@@ -503,66 +515,70 @@ msgid ""
"GNU Taler WebExtension now or click the link if your WebExtension have the "
"\"Inject Taler support\" option enabled."
msgstr ""
+"Якщо ви використовуєте браузер на комп'ютері, вам слід зараз отримати доступ "
+"до свого гаманця за допомогою розширення GNU Taler WebExtension або "
+"натиснути посилання, якщо у вашому розширенні WebExtension увімкнено опцію «"
+"Інтегрувати підтримку Taler»."
#: src/pages/OperationState/views.tsx:417
#, c-format
msgid "Start"
-msgstr ""
+msgstr "Старт"
#: src/pages/OperationState/views.tsx:426
#, c-format
msgid "On a mobile phone"
-msgstr ""
+msgstr "На мобільному телефоні"
#: src/pages/OperationState/views.tsx:431
#, c-format
msgid "Scan the QR code with your mobile device."
-msgstr ""
+msgstr "Скануйте QR-код за допомогою вашого мобільного пристрою."
#: src/pages/WalletWithdrawForm.tsx:73
#, c-format
msgid "There is an operation already"
-msgstr ""
+msgstr "Операція вже існує"
#: src/pages/WalletWithdrawForm.tsx:75
#, c-format
msgid "Complete or cancel the operation in"
-msgstr ""
+msgstr "Завершіть або скасуйте операцію в"
#: src/pages/WalletWithdrawForm.tsx:84
#, c-format
msgid "this page"
-msgstr ""
+msgstr "цій сторонці"
#: src/pages/WalletWithdrawForm.tsx:101
#, c-format
msgid "invalid"
-msgstr ""
+msgstr "недійсно"
#: src/pages/WalletWithdrawForm.tsx:116
#, c-format
msgid "Server responded with an invalid withdraw URI"
-msgstr ""
+msgstr "Сервер відповів недійсним URI для зняття коштів"
#: src/pages/WalletWithdrawForm.tsx:117
#, c-format
msgid "Withdraw URI: %1$s"
-msgstr ""
+msgstr "URI для зняття коштів: %1$s"
#: src/pages/WalletWithdrawForm.tsx:132
#, c-format
msgid "The operation was rejected due to insufficient funds"
-msgstr ""
+msgstr "Операцію було відхилено через брак коштів"
#: src/pages/WalletWithdrawForm.tsx:253
#, c-format
msgid "Continue"
-msgstr ""
+msgstr "Продовжити"
#: src/pages/WalletWithdrawForm.tsx:282
#, c-format
msgid "Prepare your wallet"
-msgstr ""
+msgstr "Підготуйте свій гаманець"
#: src/pages/WalletWithdrawForm.tsx:285
#, c-format
@@ -570,56 +586,60 @@ msgid ""
"After using your wallet you will need to confirm or cancel the operation on this "
"site."
msgstr ""
+"Після використання вашого гаманця Вам потрібно буде підтвердити або "
+"скасувати операцію на цьому сайті."
#: src/pages/WalletWithdrawForm.tsx:295
#, c-format
msgid "You need a GNU Taler Wallet"
-msgstr ""
+msgstr "Вам потрібен гаманець GNU Taler"
#: src/pages/WalletWithdrawForm.tsx:300
#, c-format
msgid "If you don't have one yet you can follow the instruction in"
-msgstr ""
+msgstr "Якщо у вас його ще немає, ви можете дотримуватися інструкцій у"
#: src/pages/PaymentOptions.tsx:55
#, c-format
msgid "Send money"
-msgstr ""
+msgstr "Надіслати гроші"
#: src/pages/PaymentOptions.tsx:73
#, c-format
msgid "to a %1$s wallet"
-msgstr ""
+msgstr "до гаманця %1$s"
#: src/pages/PaymentOptions.tsx:95
#, c-format
msgid "Withdraw digital money into your mobile wallet or browser extension"
-msgstr ""
+msgstr "Зніміть цифрові гроші у Ваш мобільний гаманець або розширення браузера"
#: src/pages/PaymentOptions.tsx:109
#, c-format
msgid "operation ready"
-msgstr ""
+msgstr "операція готова"
#: src/pages/PaymentOptions.tsx:129
#, c-format
msgid "to another bank account"
-msgstr ""
+msgstr "на інший банківський рахунок"
#: src/pages/PaymentOptions.tsx:149
#, c-format
msgid "Make a wire transfer to an account with known bank account number."
msgstr ""
+"Здійсніть банківський переказ на рахунок із відомим номером банківського "
+"рахунку."
#: src/pages/PaymentOptions.tsx:171
#, c-format
msgid "Transfer details"
-msgstr ""
+msgstr "Деталі переказу"
#: src/pages/AccountPage/views.tsx:41
#, c-format
msgid "This is a demo bank"
-msgstr ""
+msgstr "Це демонстраційний банк"
#: src/pages/AccountPage/views.tsx:46
#, c-format
@@ -628,346 +648,354 @@ msgid ""
"In addition to using your own bank account, you can also see the transaction "
"history of some %1$s."
msgstr ""
+"Ця частина демонстрації показує, як працював би банк, що безпосередньо "
+"підтримує Taler. Окрім використання вашого власного банківського рахунку, ви "
+"також можете переглянути історію транзакцій деяких %1$s."
#: src/pages/AccountPage/views.tsx:53
#, c-format
msgid "This part of the demo shows how a bank that supports Taler directly would work."
msgstr ""
+"Ця частина демонстрації показує, як працював би банк, що безпосередньо "
+"підтримує Taler."
#: src/pages/AccountPage/views.tsx:70
#, c-format
msgid "Pending account delete operation"
-msgstr ""
+msgstr "Очікування операції видалення облікового запису"
#: src/pages/AccountPage/views.tsx:72
#, c-format
msgid "Pending account update operation"
-msgstr ""
+msgstr "Очікування операції оновлення облікового запису"
#: src/pages/AccountPage/views.tsx:74
#, c-format
msgid "Pending password update operation"
-msgstr ""
+msgstr "Очікування операції оновлення пароля"
#: src/pages/AccountPage/views.tsx:76
#, c-format
msgid "Pending transaction operation"
-msgstr ""
+msgstr "Очікування операції транзакції"
#: src/pages/AccountPage/views.tsx:78
#, c-format
msgid "Pending withdrawal operation"
-msgstr ""
+msgstr "Очікування операції зняття коштів"
#: src/pages/AccountPage/views.tsx:80
#, c-format
msgid "Pending cashout operation"
-msgstr ""
+msgstr "Очікування операції зняття готівки"
#: src/pages/AccountPage/views.tsx:91
#, c-format
msgid "You can complete or cancel the operation in"
-msgstr ""
+msgstr "Ви можете завершити або скасувати операцію в"
#: src/pages/BankFrame.tsx:64
#, c-format
msgid "Internal error, please report."
-msgstr ""
+msgstr "Внутрішня помилка, будь ласка, повідомте про це."
#: src/pages/BankFrame.tsx:100
#, c-format
msgid "Preferences"
-msgstr ""
+msgstr "Налаштування"
#: src/pages/BankFrame.tsx:184
#, c-format
msgid "Welcome, %1$s"
-msgstr ""
+msgstr "Вітаємо, %1$s"
#: src/pages/WireTransfer.tsx:79
#, c-format
msgid "Make a wire transfer"
-msgstr ""
+msgstr "Здійснити банківський переказ"
#: src/pages/admin/AccountList.tsx:72
#, c-format
msgid "Accounts"
-msgstr ""
+msgstr "Рахунки"
#: src/pages/admin/AccountList.tsx:75
#, c-format
msgid "A list of all business account in the bank."
-msgstr ""
+msgstr "Список усіх бізнес-рахунків у банку."
#: src/pages/admin/AccountList.tsx:86
#, c-format
msgid "Create account"
-msgstr ""
+msgstr "Створити обліковий запис"
#: src/pages/admin/AccountList.tsx:106
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Назва"
#: src/pages/admin/AccountList.tsx:110
#, c-format
msgid "Balance"
-msgstr ""
+msgstr "Баланс"
#: src/pages/admin/AccountList.tsx:112
#, c-format
msgid "Actions"
-msgstr ""
+msgstr "Дії"
#: src/pages/admin/AccountList.tsx:151
#, c-format
msgid "unknown"
-msgstr ""
+msgstr "невідомо"
#: src/pages/admin/AccountList.tsx:170
#, c-format
msgid "change password"
-msgstr ""
+msgstr "змінити пароль"
#: src/pages/admin/AccountList.tsx:179
#, c-format
msgid "cashouts"
-msgstr ""
+msgstr "виплати готівкою"
#: src/pages/admin/AccountList.tsx:189
#, c-format
msgid "remove"
-msgstr ""
+msgstr "видалити"
#: src/pages/admin/AdminHome.tsx:168
#, c-format
msgid "Cashout not implemented"
-msgstr ""
+msgstr "Зняття готівки не реалізовано"
#: src/pages/admin/AdminHome.tsx:184
#, c-format
msgid "Select a section"
-msgstr ""
+msgstr "Оберіть розділ"
#: src/pages/admin/AdminHome.tsx:202
#, c-format
msgid "Last hour"
-msgstr ""
+msgstr "Остання година"
#: src/pages/admin/AdminHome.tsx:208
#, c-format
msgid "Last day"
-msgstr ""
+msgstr "Останній день"
#: src/pages/admin/AdminHome.tsx:216
#, c-format
msgid "Last month"
-msgstr ""
+msgstr "Останній місяць"
#: src/pages/admin/AdminHome.tsx:222
#, c-format
msgid "Last year"
-msgstr ""
+msgstr "Останній рік"
#: src/pages/admin/AdminHome.tsx:310
#, c-format
msgid "Last Year"
-msgstr ""
+msgstr "Попередній рік"
#: src/pages/admin/AdminHome.tsx:325
#, c-format
msgid "Trading volume on %1$s compared to %2$s"
-msgstr ""
+msgstr "Обсяг торгів на %1$s порівняно з %2$s"
#: src/pages/admin/AdminHome.tsx:342
#, c-format
msgid "Cashin"
-msgstr ""
+msgstr "Поповнення готівкою"
#: src/pages/admin/AdminHome.tsx:352
#, c-format
msgid "Cashout"
-msgstr ""
+msgstr "Виплати готівкою"
#: src/pages/admin/AdminHome.tsx:364
#, c-format
msgid "Payin"
-msgstr ""
+msgstr "Внесення коштів"
#: src/pages/admin/AdminHome.tsx:374
#, c-format
msgid "Payout"
-msgstr ""
+msgstr "Виплата"
#: src/pages/admin/AdminHome.tsx:388
#, c-format
msgid "download stats as CSV"
-msgstr ""
+msgstr "завантажити статистику у форматі CSV"
#: src/pages/admin/AdminHome.tsx:494
#, c-format
msgid "Decreased by"
-msgstr ""
+msgstr "Зменшилось на"
#: src/pages/admin/AdminHome.tsx:498
#, c-format
msgid "Increased by"
-msgstr ""
+msgstr "Збільшилось на"
#: src/pages/DownloadStats.tsx:89
#, c-format
msgid "Download bank stats"
-msgstr ""
+msgstr "Завантажити статистику банку"
#: src/pages/DownloadStats.tsx:110
#, c-format
msgid "Include hour metric"
-msgstr ""
+msgstr "Включити часову метрику"
#: src/pages/DownloadStats.tsx:143
#, c-format
msgid "Include day metric"
-msgstr ""
+msgstr "Включити добову метрику"
#: src/pages/DownloadStats.tsx:173
#, c-format
msgid "Include month metric"
-msgstr ""
+msgstr "Включити місячну метрику"
#: src/pages/DownloadStats.tsx:206
#, c-format
msgid "Include year metric"
-msgstr ""
+msgstr "Включити річну метрику"
#: src/pages/DownloadStats.tsx:239
#, c-format
msgid "Include table header"
-msgstr ""
+msgstr "Включити заголовок таблиці"
#: src/pages/DownloadStats.tsx:272
#, c-format
msgid "Add previous metric for compare"
-msgstr ""
+msgstr "Додати попередню метрику для порівняння"
#: src/pages/DownloadStats.tsx:307
#, c-format
msgid "Fail on first error"
-msgstr ""
+msgstr "Збій на першій помилці"
#: src/pages/DownloadStats.tsx:364
#, c-format
msgid "Download"
-msgstr ""
+msgstr "Завантажити"
#: src/pages/DownloadStats.tsx:381
#, c-format
msgid "downloading... %1$s"
-msgstr ""
+msgstr "завантаження...%1$s"
#: src/pages/DownloadStats.tsx:399
#, c-format
msgid "Download completed"
-msgstr ""
+msgstr "Завантаження завершено"
#: src/pages/DownloadStats.tsx:400
#, c-format
msgid "click here to save the file in your computer"
-msgstr ""
+msgstr "натисніть тут, щоб зберегти файл на вашому комп'ютері"
#: src/pages/PublicHistoriesPage.tsx:78
#, c-format
msgid "History of public accounts"
-msgstr ""
+msgstr "Історія публічних рахунків"
#: src/pages/RegistrationPage.tsx:48
#, c-format
msgid "Currently, the bank is not accepting new registrations!"
-msgstr ""
+msgstr "Наразі банк не приймає нові реєстрації!"
#: src/pages/RegistrationPage.tsx:87
#, c-format
msgid "Missing name"
-msgstr ""
+msgstr "Імʼя відсутнє"
#: src/pages/RegistrationPage.tsx:91
#, c-format
msgid "Use letters and numbers only, and start with a lowercase letter"
-msgstr ""
+msgstr "Використовуйте лише літери та цифри, і починайте з малої літери"
#: src/pages/RegistrationPage.tsx:107
#, c-format
msgid "Passwords don't match"
-msgstr ""
+msgstr "Паролі не збігаються"
#: src/pages/RegistrationPage.tsx:130
#, c-format
msgid "Server replied with invalid phone or email."
-msgstr ""
+msgstr "Сервер відповів, що номер телефону або електронна пошта недійсні."
#: src/pages/RegistrationPage.tsx:137
#, c-format
msgid "Registration is disabled because the bank ran out of bonus credit."
-msgstr ""
+msgstr "Реєстрація відключена, оскільки банк вичерпав бонусний кредит."
#: src/pages/RegistrationPage.tsx:144
#, c-format
msgid "No enough permission to create that account."
-msgstr ""
+msgstr "Недостатньо прав для створення цього облікового запису."
#: src/pages/RegistrationPage.tsx:151
#, c-format
msgid "That account id is already taken."
-msgstr ""
+msgstr "Цей ідентифікатор облікового запису вже зайнятий."
#: src/pages/RegistrationPage.tsx:158
#, c-format
msgid "That username is already taken."
-msgstr ""
+msgstr "Це ім'я користувача вже зайняте."
#: src/pages/RegistrationPage.tsx:165
#, c-format
msgid "That username can't be used because is reserved."
msgstr ""
+"Це ім'я користувача не можна використовувати, оскільки воно зарезервоване."
#: src/pages/RegistrationPage.tsx:172
#, c-format
msgid "Only admin is allow to set debt limit."
-msgstr ""
+msgstr "Лише адміністратору дозволено встановлювати ліміт боргу."
#: src/pages/RegistrationPage.tsx:179
#, c-format
msgid "No information for the selected authentication channel."
-msgstr ""
+msgstr "Немає інформації про обраний канал автентифікації."
#: src/pages/RegistrationPage.tsx:186
#, c-format
msgid "Authentication channel is not supported."
-msgstr ""
+msgstr "Канал автентифікації не підтримується."
#: src/pages/RegistrationPage.tsx:193
#, c-format
msgid "Only admin can create accounts with second factor authentication."
msgstr ""
+"Лише адміністратор може створювати облікові записи з двофакторною "
+"автентифікацією."
#: src/pages/RegistrationPage.tsx:233
#, c-format
msgid "Account registration"
-msgstr ""
+msgstr "Реєстрація облікового запису"
#: src/pages/RegistrationPage.tsx:315
#, c-format
msgid "Repeat password"
-msgstr ""
+msgstr "Повторіть пароль"
#: src/pages/RegistrationPage.tsx:457
#, c-format
msgid "Create a random temporary user"
-msgstr ""
+msgstr "Створити випадкового тимчасового користувача"
#: src/pages/QrCodeSection.tsx:110
#, c-format
msgid "If you have a Taler wallet installed in this device"
-msgstr ""
+msgstr "Якщо на цьому пристрої встановлено гаманець Taler"
#: src/pages/QrCodeSection.tsx:116
#, c-format
@@ -976,26 +1004,29 @@ msgid ""
"applies). If you still don't have one you can install it following instructions "
"in"
msgstr ""
+"Ви побачите деталі операції у вашомугаманці, включаючи комісії (якщо є). "
+"Якщо у вас його ще немає, ви можете встановити його, дотримуючись інструкцій "
+"у"
#: src/pages/QrCodeSection.tsx:143
#, c-format
msgid "Withdraw"
-msgstr ""
+msgstr "Зняття коштів"
#: src/pages/QrCodeSection.tsx:152
#, c-format
msgid "Or if you have the wallet in another device"
-msgstr ""
+msgstr "Або якщо у вас є гаманець на іншому пристрої"
#: src/pages/QrCodeSection.tsx:157
#, c-format
msgid "Scan the QR below to start the withdrawal."
-msgstr ""
+msgstr "Скануйте QR-код нижче, щоб розпочати зняття коштів."
#: src/pages/WithdrawalQRCode.tsx:79
#, c-format
msgid "Operation aborted"
-msgstr ""
+msgstr "Операцію скасовано"
#: src/pages/WithdrawalQRCode.tsx:82
#, c-format
@@ -1003,31 +1034,37 @@ msgid ""
"The wire transfer to the Taler Exchange operator's account was aborted, your "
"balance was not affected."
msgstr ""
+"Банківський переказ на рахунок оператора Taler Exchange було скасовано, ваш "
+"баланс не постраждав."
#: src/pages/WithdrawalQRCode.tsx:88
#, c-format
msgid "You can close this page now or continue to the account page."
msgstr ""
+"Ви можете закрити цю сторінку зараз або перейти на сторінку облікового "
+"запису."
#: src/pages/WithdrawalQRCode.tsx:147
#, c-format
msgid "Done"
-msgstr ""
+msgstr "Готово"
#: src/pages/WithdrawalQRCode.tsx:158
#, c-format
msgid "Operation canceled"
-msgstr ""
+msgstr "Операція відхилена"
#: src/pages/WithdrawalQRCode.tsx:173
#, c-format
msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
msgstr ""
+"Операція позначена як 'вибрана', але деякий крок у процесі зняття коштів не "
+"вдалося виконати"
#: src/pages/WithdrawalQRCode.tsx:175
#, c-format
msgid "The account is selected but no withdrawal identification found."
-msgstr ""
+msgstr "Обліковий запис вибрано, але ідентифікацію зняття коштів не знайдено."
#: src/pages/WithdrawalQRCode.tsx:188
#, c-format
@@ -1035,6 +1072,8 @@ msgid ""
"There is a withdrawal identification but no account has been selected or the "
"selected account is invalid."
msgstr ""
+"Є ідентифікація зняття коштів, але обліковий запис не вибрано або вибраний "
+"обліковий запис недійсний."
#: src/pages/WithdrawalQRCode.tsx:202
#, c-format
@@ -1042,11 +1081,13 @@ msgid ""
"No withdrawal ID found and no account has been selected or the selected account "
"is invalid."
msgstr ""
+"Ідентифікатор зняття коштів не знайдено, обліковий запис не вибрано або "
+"вибраний обліковий запис недійсний."
#: src/pages/WithdrawalQRCode.tsx:259
#, c-format
msgid "Operation not found"
-msgstr ""
+msgstr "Операцію не знайдено"
#: src/pages/WithdrawalQRCode.tsx:263
#, c-format
@@ -1054,221 +1095,226 @@ msgid ""
"This operation is not known by the server. The operation id is wrong or the "
"server deleted the operation information before reaching here."
msgstr ""
+"Ця операція невідома серверу. Ідентифікатор операції неправильний або сервер "
+"видалив інформацію про операцію до її завершення."
#: src/pages/WithdrawalQRCode.tsx:278
#, c-format
msgid "Cotinue to dashboard"
-msgstr ""
+msgstr "Перейти до панелі керування"
#: src/pages/SolveChallengePage.tsx:98
#, c-format
msgid "Cashout not found. It may be also mean that it was already aborted."
msgstr ""
+"Зняття готівки не знайдено. Це також може означати, що його вже скасовано."
#: src/pages/SolveChallengePage.tsx:136
#, c-format
msgid "Challenge not found."
-msgstr ""
+msgstr "Виклик не знайдено."
#: src/pages/SolveChallengePage.tsx:143
#, c-format
msgid "This user is not authorized to complete this challenge."
-msgstr ""
+msgstr "Цей користувач не має права виконати цей виклик."
#: src/pages/SolveChallengePage.tsx:150
#, c-format
msgid "Too many attempts, try another code."
-msgstr ""
+msgstr "Забагато спроб, спробуйте інший код."
#: src/pages/SolveChallengePage.tsx:157
#, c-format
msgid "The confirmation code is wrong, try again."
-msgstr ""
+msgstr "Код підтвердження неправильний, спробуйте ще раз."
#: src/pages/SolveChallengePage.tsx:164
#, c-format
msgid "The operation expired."
-msgstr ""
+msgstr "Термін дії операції закінчився."
#: src/pages/SolveChallengePage.tsx:197
#, c-format
msgid "The operation failed."
-msgstr ""
+msgstr "Операція не вдалася."
#: src/pages/SolveChallengePage.tsx:212
#, c-format
msgid "The operation needs another confirmation to complete."
-msgstr ""
+msgstr "Для завершення операції потрібне ще одне підтвердження."
#: src/pages/SolveChallengePage.tsx:224
#, c-format
msgid "Account delete"
-msgstr ""
+msgstr "Видалення облікового запису"
#: src/pages/SolveChallengePage.tsx:226
#, c-format
msgid "Account update"
-msgstr ""
+msgstr "Оновлення облікового запису"
#: src/pages/SolveChallengePage.tsx:228
#, c-format
msgid "Password update"
-msgstr ""
+msgstr "Оновлення пароля"
#: src/pages/SolveChallengePage.tsx:230
#, c-format
msgid "Wire transfer"
-msgstr ""
+msgstr "Банківський переказ"
#: src/pages/SolveChallengePage.tsx:232
#, c-format
msgid "Withdrawal"
-msgstr ""
+msgstr "Зняття"
#: src/pages/SolveChallengePage.tsx:248
#, c-format
msgid "Confirm the operation"
-msgstr ""
+msgstr "Підтвердити операцію"
#: src/pages/SolveChallengePage.tsx:271
#, c-format
msgid "Enter the confirmation code"
-msgstr ""
+msgstr "Введіть код підтвердження"
#: src/pages/SolveChallengePage.tsx:313
#, c-format
msgid "Confirm"
-msgstr ""
+msgstr "Підтвердити"
#: src/pages/SolveChallengePage.tsx:348
#, c-format
msgid "Send again"
-msgstr ""
+msgstr "Відправити знову"
#: src/pages/SolveChallengePage.tsx:359
#, c-format
msgid "Send code"
-msgstr ""
+msgstr "Відправити код"
#: src/pages/SolveChallengePage.tsx:369
#, c-format
msgid "Operation details"
-msgstr ""
+msgstr "Деталі операції"
#: src/pages/SolveChallengePage.tsx:529
#, c-format
msgid "Challenge details"
-msgstr ""
+msgstr "Деталі підтвердження"
#: src/pages/SolveChallengePage.tsx:536
#, c-format
msgid "Sent at"
-msgstr ""
+msgstr "Надіслано о"
#: src/pages/SolveChallengePage.tsx:551
#, c-format
msgid "To phone"
-msgstr ""
+msgstr "На телефон"
#: src/pages/SolveChallengePage.tsx:553
#, c-format
msgid "To email"
-msgstr ""
+msgstr "На email"
#: src/pages/WithdrawalOperationPage.tsx:49
#, c-format
msgid "The Withdrawal URI is not valid"
-msgstr ""
+msgstr "URI для зняття коштів недійсний"
#: src/components/Cashouts/views.tsx:100
#, c-format
msgid "Latest cashouts"
-msgstr ""
+msgstr "Останні зняття готівки"
#: src/components/Cashouts/views.tsx:111
#, c-format
msgid "Created"
-msgstr ""
+msgstr "Створено"
#: src/components/Cashouts/views.tsx:115
#, c-format
msgid "Total debit"
-msgstr ""
+msgstr "Загальний дебет"
#: src/components/Cashouts/views.tsx:119
#, c-format
msgid "Total credit"
-msgstr ""
+msgstr "Загальний кредит"
#: src/pages/ProfileNavigation.tsx:70
#, c-format
msgid "Details"
-msgstr ""
+msgstr "Деталі"
#: src/pages/ProfileNavigation.tsx:74
#, c-format
msgid "Delete"
-msgstr ""
+msgstr "Видалити"
#: src/pages/ProfileNavigation.tsx:78
#, c-format
msgid "Credentials"
-msgstr ""
+msgstr "Облікові дані"
#: src/pages/ProfileNavigation.tsx:82
#, c-format
msgid "Cashouts"
-msgstr ""
+msgstr "Зняття готівки"
#: src/pages/business/CreateCashout.tsx:95
#, c-format
msgid "Unable to create a cashout"
-msgstr ""
+msgstr "Не вдалося створити зняття готівки"
#: src/pages/business/CreateCashout.tsx:96
#, c-format
msgid "The bank configuration does not support cashout operations."
-msgstr ""
+msgstr "Конфігурація банку не підтримує операції зі зняття готівки."
#: src/pages/business/CreateCashout.tsx:223
#, c-format
msgid "need to be higher due to fees"
-msgstr ""
+msgstr "повинна бути вищою через комісії"
#: src/pages/business/CreateCashout.tsx:225
#, c-format
msgid "the total transfer at destination will be zero"
-msgstr ""
+msgstr "загальна сума переказу на місці призначення буде нульовою"
#: src/pages/business/CreateCashout.tsx:250
#, c-format
msgid "Cashout created"
-msgstr ""
+msgstr "Зняття готівки створено"
#: src/pages/business/CreateCashout.tsx:272
#, c-format
msgid "Duplicated request detected, check if the operation succeeded or try again."
msgstr ""
+"Виявлено повторний запит, перевірте, чи була операція успішною, або "
+"спробуйте ще раз."
#: src/pages/business/CreateCashout.tsx:279
#, c-format
msgid "The conversion rate was incorrectly applied"
-msgstr ""
+msgstr "Курс обміну було застосовано неправильно"
#: src/pages/business/CreateCashout.tsx:286
#, c-format
msgid "The account does not have sufficient funds"
-msgstr ""
+msgstr "На рахунку недостатньо коштів"
#: src/pages/business/CreateCashout.tsx:293
#, c-format
msgid "Cashouts are not supported"
-msgstr ""
+msgstr "Зняття готівки не підтримується"
#: src/pages/business/CreateCashout.tsx:300
#, c-format
msgid "Missing cashout URI in the profile"
-msgstr ""
+msgstr "Відсутній URI зняття готівки в профілі"
#: src/pages/business/CreateCashout.tsx:307
#, c-format
@@ -1276,66 +1322,68 @@ msgid ""
"Sending the confirmation message failed, retry later or contact the "
"administrator."
msgstr ""
+"Не вдалося надіслати повідомлення з підтвердженням, спробуйте пізніше або "
+"зверніться до адміністратора."
#: src/pages/business/CreateCashout.tsx:339
#, c-format
msgid "Conversion rate"
-msgstr ""
+msgstr "Обмінний курс"
#: src/pages/business/CreateCashout.tsx:360
#, c-format
msgid "Fee"
-msgstr ""
+msgstr "Комісія"
#: src/pages/business/CreateCashout.tsx:374
#, c-format
msgid "To account"
-msgstr ""
+msgstr "На рахунок"
#: src/pages/business/CreateCashout.tsx:381
#, c-format
msgid "No cashout account"
-msgstr ""
+msgstr "Відсутній рахунок для зняття готівки"
#: src/pages/business/CreateCashout.tsx:382
#, c-format
msgid "Before doing a cashout you need to complete your profile"
-msgstr ""
+msgstr "Перш ніж здійснити зняття готівки, вам потрібно заповнити свій профіль"
#: src/pages/business/CreateCashout.tsx:440
#, c-format
msgid "Amount to send"
-msgstr ""
+msgstr "Сума для відправлення"
#: src/pages/business/CreateCashout.tsx:441
#, c-format
msgid "Amount to receive"
-msgstr ""
+msgstr "Сума до отримання"
#: src/pages/business/CreateCashout.tsx:490
#, c-format
msgid "Total cost"
-msgstr ""
+msgstr "Загальна вартість"
#: src/pages/business/CreateCashout.tsx:505
#, c-format
msgid "Balance left"
-msgstr ""
+msgstr "Залишок балансу"
#: src/pages/business/CreateCashout.tsx:520
#, c-format
msgid "Before fee"
-msgstr ""
+msgstr "Комісія до"
#: src/pages/business/CreateCashout.tsx:533
#, c-format
msgid "Total cashout transfer"
-msgstr ""
+msgstr "Загальна сума зняття готівки"
#: src/pages/business/CreateCashout.tsx:553
#, c-format
msgid "No cashout channel available"
-msgstr ""
+msgstr "Канал зняття готівки недоступний"
#: src/pages/business/CreateCashout.tsx:555
#, c-format
@@ -1343,166 +1391,172 @@ msgid ""
"Before doing a cashout the server need to provide an second channel to confirm "
"the operation"
msgstr ""
+"Перш ніж здійснити зняття готівки, сервер повинен надати другий канал для "
+"підтвердження операції"
#: src/pages/business/CreateCashout.tsx:567
#, c-format
msgid "Second factor authentication"
-msgstr ""
+msgstr "Двофакторна автентифікація"
#: src/pages/business/CreateCashout.tsx:598
#, c-format
msgid "Email"
-msgstr ""
+msgstr "Email"
#: src/pages/business/CreateCashout.tsx:600
#, c-format
msgid "add a email in your profile to enable this option"
-msgstr ""
+msgstr "додайте електронну пошту у вашому профілі, щоб увімкнути цю опцію"
#: src/pages/business/CreateCashout.tsx:646
#, c-format
msgid "SMS"
-msgstr ""
+msgstr "SMS"
#: src/pages/business/CreateCashout.tsx:648
#, c-format
msgid "add a phone number in your profile to enable this option"
-msgstr ""
+msgstr "додайте номер телефону у вашому профілі, щоб увімкнути цю опцію"
#: src/pages/account/CashoutListForAccount.tsx:52
#, c-format
msgid "Cashout for account %1$s"
-msgstr ""
+msgstr "Зняття готівки для облікового запису %1$s"
#: src/pages/admin/AccountForm.tsx:165
#, c-format
msgid "it doesn't have the pattern of an IBAN number"
-msgstr ""
+msgstr "він не відповідає шаблону номера IBAN"
#: src/pages/admin/AccountForm.tsx:185
#, c-format
msgid "it doesn't have the pattern of an email"
-msgstr ""
+msgstr "він не відповідає шаблону електронної пошти"
#: src/pages/admin/AccountForm.tsx:190
#, c-format
msgid "should start with +"
-msgstr ""
+msgstr "повинен починатися з +"
#: src/pages/admin/AccountForm.tsx:192
#, c-format
msgid "phone number can't have other than numbers"
-msgstr ""
+msgstr "номер телефону повинен містити лише цифри"
#: src/pages/admin/AccountForm.tsx:329
#, c-format
msgid "account identification in the bank"
-msgstr ""
+msgstr "ідентифікація облікового запису в банку"
#: src/pages/admin/AccountForm.tsx:365
#, c-format
msgid "name of the person owner the account"
-msgstr ""
+msgstr "ім'я особи, якій належить обліковий запис"
#: src/pages/admin/AccountForm.tsx:374
#, c-format
msgid "Internal IBAN"
-msgstr ""
+msgstr "Внутрішній IBAN"
#: src/pages/admin/AccountForm.tsx:377
#, c-format
msgid "if empty a random account number will be assigned"
-msgstr ""
+msgstr "якщо порожньо, буде призначено випадковий номер рахунку"
#: src/pages/admin/AccountForm.tsx:378
#, c-format
msgid "account identification for bank transfer"
-msgstr ""
+msgstr "ідентифікація облікового запису для банківського переказу"
#: src/pages/admin/AccountForm.tsx:423
#, c-format
msgid "Phone"
-msgstr ""
+msgstr "Телефон"
#: src/pages/admin/AccountForm.tsx:451
#, c-format
msgid "Cashout IBAN"
-msgstr ""
+msgstr "IBAN зняття готівки"
#: src/pages/admin/AccountForm.tsx:452
#, c-format
msgid "account number where the money is going to be sent when doing cashouts"
-msgstr ""
+msgstr "номер рахунку, на який будуть відправлені гроші при знятті готівки"
#: src/pages/admin/AccountForm.tsx:470
#, c-format
msgid "Max debt"
-msgstr ""
+msgstr "Максимальний борг"
#: src/pages/admin/AccountForm.tsx:494
#, c-format
msgid "how much is user able to transfer after zero balance"
-msgstr ""
+msgstr "Скільки користувач може перевести після досягнення нульового балансу"
#: src/pages/admin/AccountForm.tsx:508
#, c-format
msgid "Is this a Taler Exchange?"
-msgstr ""
+msgstr "Чи це обмінний пункт Taler?"
#: src/pages/admin/AccountForm.tsx:549
#, c-format
msgid "This server doesn't support second factor authentication."
-msgstr ""
+msgstr "Цей сервер не підтримує двофакторну автентифікацію."
#: src/pages/admin/AccountForm.tsx:560
#, c-format
msgid "Enable second factor authentication"
-msgstr ""
+msgstr "Увімкнути двофакторну автентифікацію"
#: src/pages/admin/AccountForm.tsx:596
#, c-format
msgid "Using email"
-msgstr ""
+msgstr "Використовуючи email"
#: src/pages/admin/AccountForm.tsx:654
#, c-format
msgid "Using SMS"
-msgstr ""
+msgstr "Використовуючи SMS"
#: src/pages/admin/AccountForm.tsx:691
#, c-format
msgid "Is this account public?"
-msgstr ""
+msgstr "Цей обліковий запис є публічним?"
#: src/pages/admin/AccountForm.tsx:719
#, c-format
msgid "public accounts have their balance publicly accessible"
-msgstr ""
+msgstr "публічні рахунки мають публічно доступний баланс"
#: src/pages/account/ShowAccountDetails.tsx:100
#, c-format
msgid "Account updated"
-msgstr ""
+msgstr "Рахунок оновлено"
#: src/pages/account/ShowAccountDetails.tsx:107
#, c-format
msgid "The rights to change the account are not sufficient"
-msgstr ""
+msgstr "Недостатньо прав для зміни облікового запису"
#: src/pages/account/ShowAccountDetails.tsx:114
#, c-format
msgid "The username was not found"
-msgstr ""
+msgstr "Ім'я користувача не знайдено"
#: src/pages/account/ShowAccountDetails.tsx:121
#, c-format
msgid "You can't change the legal name, please contact the your account administrator."
msgstr ""
+"Ви не можете змінити юридичне ім'я, будь ласка, зверніться до адміністратора "
+"вашого облікового запису."
#: src/pages/account/ShowAccountDetails.tsx:128
#, c-format
msgid "You can't change the debt limit, please contact the your account administrator."
msgstr ""
+"Ви не можете змінити ліміт боргу, будь ласка, зверніться до адміністратора "
+"вашого облікового запису."
#: src/pages/account/ShowAccountDetails.tsx:135
#, c-format
@@ -1510,36 +1564,38 @@ msgid ""
"You can't change the cashout address, please contact the your account "
"administrator."
msgstr ""
+"Ви не можете змінити адресу зняття готівки, будь ласка, зверніться до "
+"адміністратора вашого облікового запису."
#: src/pages/account/ShowAccountDetails.tsx:177
#, c-format
msgid "Account \"%1$s\""
-msgstr ""
+msgstr "Рахунок \"%1$s\""
#: src/pages/account/ShowAccountDetails.tsx:190
#, c-format
msgid "Change details"
-msgstr ""
+msgstr "Зміна реквізитів"
#: src/pages/account/ShowAccountDetails.tsx:235
#, c-format
msgid "Update"
-msgstr ""
+msgstr "Оновити"
#: src/pages/account/UpdateAccountPassword.tsx:78
#, c-format
msgid "password doesn't match"
-msgstr ""
+msgstr "пароль не співпадає"
#: src/pages/account/UpdateAccountPassword.tsx:95
#, c-format
msgid "Password changed"
-msgstr ""
+msgstr "Пароль змінено"
#: src/pages/account/UpdateAccountPassword.tsx:102
#, c-format
msgid "Not authorized to change the password, maybe the session is invalid."
-msgstr ""
+msgstr "Немає прав для зміни пароля, можливо, сеанс недійсний."
#: src/pages/account/UpdateAccountPassword.tsx:112
#, c-format
@@ -1547,46 +1603,48 @@ msgid ""
"You need to provide the old password. If you don't have it contact your account "
"administrator."
msgstr ""
+"Вам потрібно надати старий пароль. Якщо у вас його немає, зверніться до "
+"адміністратора вашого облікового запису."
#: src/pages/account/UpdateAccountPassword.tsx:117
#, c-format
msgid "Your current password doesn't match, can't change to a new password."
-msgstr ""
+msgstr "Ваш поточний пароль не збігається, не вдалося змінити на новий пароль."
#: src/pages/account/UpdateAccountPassword.tsx:149
#, c-format
msgid "Update password"
-msgstr ""
+msgstr "Оновити пароль"
#: src/pages/account/UpdateAccountPassword.tsx:167
#, c-format
msgid "New password"
-msgstr ""
+msgstr "Новий пароль"
#: src/pages/account/UpdateAccountPassword.tsx:195
#, c-format
msgid "Type it again"
-msgstr ""
+msgstr "Введіть його ще раз"
#: src/pages/account/UpdateAccountPassword.tsx:217
#, c-format
msgid "repeat the same password"
-msgstr ""
+msgstr "повторіть той самий пароль"
#: src/pages/account/UpdateAccountPassword.tsx:227
#, c-format
msgid "Current password"
-msgstr ""
+msgstr "Поточний пароль"
#: src/pages/account/UpdateAccountPassword.tsx:248
#, c-format
msgid "your current password, for security"
-msgstr ""
+msgstr "ваш поточний пароль, для безпеки"
#: src/pages/account/UpdateAccountPassword.tsx:272
#, c-format
msgid "Change"
-msgstr ""
+msgstr "Змінити"
#: src/pages/admin/CreateNewAccount.tsx:74
#, c-format
@@ -1594,61 +1652,65 @@ msgid ""
"Account created with password \"%1$s\". The user must change the password on the "
"next login."
msgstr ""
+"Обліковий запис створено з паролем \"%1$s\". Користувач повинен змінити "
+"пароль під час наступного входу."
#: src/pages/admin/CreateNewAccount.tsx:83
#, c-format
msgid "Server replied that phone or email is invalid"
-msgstr ""
+msgstr "Сервер відповів, що номер телефону або електронна пошта недійсні"
#: src/pages/admin/CreateNewAccount.tsx:90
#, c-format
msgid "The rights to perform the operation are not sufficient"
-msgstr ""
+msgstr "Недостатньо прав для виконання операції"
#: src/pages/admin/CreateNewAccount.tsx:97
#, c-format
msgid "Account username is already taken"
-msgstr ""
+msgstr "Ім'я користувача облікового запису вже зайнято"
#: src/pages/admin/CreateNewAccount.tsx:104
#, c-format
msgid "Account id is already taken"
-msgstr ""
+msgstr "Ідентифікатор облікового запису вже зайнятий"
#: src/pages/admin/CreateNewAccount.tsx:111
#, c-format
msgid "Bank ran out of bonus credit."
-msgstr ""
+msgstr "У банку закінчився бонусний кредит."
#: src/pages/admin/CreateNewAccount.tsx:118
#, c-format
msgid "Account username can't be used because is reserved"
msgstr ""
+"Ім'я користувача облікового запису не можна використовувати, оскільки воно "
+"зарезервоване"
#: src/pages/admin/CreateNewAccount.tsx:160
#, c-format
msgid "Can't create accounts"
-msgstr ""
+msgstr "Не вдається створити рахунки"
#: src/pages/admin/CreateNewAccount.tsx:161
#, c-format
msgid "Only system admin can create accounts."
-msgstr ""
+msgstr "Лише системний адміністратор може створювати рахунки."
#: src/pages/admin/CreateNewAccount.tsx:183
#, c-format
msgid "New business account"
-msgstr ""
+msgstr "Новий бізнес рахунок"
#: src/pages/admin/CreateNewAccount.tsx:209
#, c-format
msgid "Create"
-msgstr ""
+msgstr "Створити"
#: src/pages/admin/RemoveAccount.tsx:94
#, c-format
msgid "Can't delete the account"
-msgstr ""
+msgstr "Не вдається видалити обліковий запис"
#: src/pages/admin/RemoveAccount.tsx:95
#, c-format
@@ -1656,88 +1718,90 @@ msgid ""
"The account can't be delete while still holding some balance. First make sure "
"that the owner make a complete cashout."
msgstr ""
+"Обліковий запис не можна видалити, поки на ньому є баланс. Спочатку "
+"переконайтеся, що власник зробив повне зняття коштів."
#: src/pages/admin/RemoveAccount.tsx:117
#, c-format
msgid "Account removed"
-msgstr ""
+msgstr "Обліковий запис видалено"
#: src/pages/admin/RemoveAccount.tsx:124
#, c-format
msgid "No enough permission to delete the account."
-msgstr ""
+msgstr "Недостатньо прав для видалення облікового запису."
#: src/pages/admin/RemoveAccount.tsx:131
#, c-format
msgid "The username was not found."
-msgstr ""
+msgstr "Ім'я користувача не знайдено."
#: src/pages/admin/RemoveAccount.tsx:138
#, c-format
msgid "Can't delete a reserved username."
-msgstr ""
+msgstr "Не можна видалити зарезервоване ім'я користувача."
#: src/pages/admin/RemoveAccount.tsx:145
#, c-format
msgid "Can't delete an account with balance different than zero."
-msgstr ""
+msgstr "Не можна видалити обліковий запис з балансом, відмінним від нуля."
#: src/pages/admin/RemoveAccount.tsx:170
#, c-format
msgid "name doesn't match"
-msgstr ""
+msgstr "ім'я не збігається"
#: src/pages/admin/RemoveAccount.tsx:180
#, c-format
msgid "You are going to remove the account"
-msgstr ""
+msgstr "Ви збираєтеся видалити обліковий запис"
#: src/pages/admin/RemoveAccount.tsx:182
#, c-format
msgid "This step can't be undone."
-msgstr ""
+msgstr "Цей крок не можна скасувати."
#: src/pages/admin/RemoveAccount.tsx:188
#, c-format
msgid "Deleting account \"%1$s\""
-msgstr ""
+msgstr "Видалення рахунку \"%1$s\""
#: src/pages/admin/RemoveAccount.tsx:206
#, c-format
msgid "Verification"
-msgstr ""
+msgstr "Підтвердження"
#: src/pages/admin/RemoveAccount.tsx:231
#, c-format
msgid "enter the account name that is going to be deleted"
-msgstr ""
+msgstr "введіть ім'я рахунку, який буде видалено"
#: src/pages/business/ShowCashoutDetails.tsx:49
#, c-format
msgid "cashout id should be a number"
-msgstr ""
+msgstr "ідентифікатор зняття готівки повинен бути числом"
#: src/pages/business/ShowCashoutDetails.tsx:65
#, c-format
msgid "This cashout not found. Maybe already aborted."
-msgstr ""
+msgstr "Це зняття готівки не знайдено. Можливо, його вже скасовано."
#: src/pages/business/ShowCashoutDetails.tsx:106
#, c-format
msgid "Cashout detail"
-msgstr ""
+msgstr "Деталі зняття готівки"
#: src/pages/business/ShowCashoutDetails.tsx:139
#, c-format
msgid "Debited"
-msgstr ""
+msgstr "Дебетовано"
#: src/pages/business/ShowCashoutDetails.tsx:154
#, c-format
msgid "Credited"
-msgstr ""
+msgstr "Кредитовано"
#: src/Routing.tsx:140
#, c-format
msgid "Welcome to %1$s!"
-msgstr ""
+msgstr "Ласкаво просимо до %1$s!"
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts b/packages/bank-ui/src/pages/AccountPage/index.ts
index 8a9471ef4..e96702652 100644
--- a/packages/bank-ui/src/pages/AccountPage/index.ts
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -26,6 +26,7 @@ import { LoginForm } from "../LoginForm.js";
import { useComponentState } from "./state.js";
import { InvalidIbanView, ReadyView } from "./views.js";
import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { Fragment } from "preact";
export interface Props {
account: string;
@@ -125,7 +126,11 @@ const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
login: LoginForm,
"invalid-iban": InvalidIbanView,
- "loading-error": ErrorLoadingWithDebug,
+ "loading-error": (d) => {
+ return Fragment({
+ children: [ErrorLoadingWithDebug({ error: d.error }), LoginForm({})],
+ })!;
+ },
ready: ReadyView,
};
diff --git a/packages/bank-ui/src/pages/AccountPage/views.tsx b/packages/bank-ui/src/pages/AccountPage/views.tsx
index 42892f536..93a769147 100644
--- a/packages/bank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -42,7 +42,7 @@ function ShowDemoInfo({
if (!settings.showDemoDescription) return <Fragment />;
return (
<Attention
- title={i18n.str`This is a demo bank`}
+ title={i18n.str`This is a demo`}
onClose={() => {
updateSettings("showDemoDescription", false);
}}
@@ -59,7 +59,7 @@ function ShowDemoInfo({
</i18n.Translate>
) : (
<i18n.Translate>
- This part of the demo shows how a bank that supports Taler directly
+ Here you will be able to see how a bank that supports Taler directly
would work.
</i18n.Translate>
)}
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
index db757ee07..f3c6817d3 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -64,6 +64,8 @@ export function BankFrame({
const settings = useSettingsContext();
const [preferences, updatePreferences] = usePreferences();
const [, , resetBankState] = useBankState();
+ const d = useBankCoreApiContext();
+ const config = d === undefined ? undefined : d.config;
const [error, resetError] = useErrorBoundary();
@@ -90,7 +92,7 @@ export function BankFrame({
>
<div class="bg-indigo-600 pb-32">
<Header
- title="Bank"
+ title={config?.bank_name ?? "Bank"}
iconLinkURL={settings.iconLinkURL ?? "#"}
profileURL={routeAccountDetails?.url({})}
notificationURL={
@@ -160,7 +162,6 @@ export function BankFrame({
<div class="fixed z-20 top-14 w-full">
<div class="mx-auto w-4/5">
<ToastBanner />
- {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */}
</div>
</div>
@@ -257,7 +258,7 @@ function AppActivity(): VNode {
return;
}
/**
- * all of this are ignored
+ * all of these are ignored
*/
case ObservabilityEventType.DbQueryStart:
case ObservabilityEventType.DbQueryFinishSuccess:
@@ -274,6 +275,7 @@ function AppActivity(): VNode {
case ObservabilityEventType.CryptoFinishSuccess:
case ObservabilityEventType.CryptoFinishError:
case ObservabilityEventType.Message:
+ case ObservabilityEventType.DeclareConcernsTransaction:
return;
default: {
assertUnreachable(ev);
diff --git a/packages/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts
index 38f698a04..728e653bd 100644
--- a/packages/bank-ui/src/pages/OperationState/index.ts
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -17,6 +17,7 @@
import {
AbsoluteTime,
AmountJson,
+ PaytoUri,
TalerCoreBankErrorsByMethod,
TalerError,
WithdrawUriResult,
@@ -41,6 +42,7 @@ export interface Props {
onAuthorizationRequired: () => void;
routeClose: RouteDefinition;
onAbort: () => void;
+ focus?: boolean;
routeHere: RouteDefinition<{ wopid: string }>;
}
@@ -79,6 +81,7 @@ export namespace State {
status: "ready";
error: undefined;
uri: WithdrawUriResult;
+ focus?: boolean;
onAbort: () => Promise<
TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
>;
@@ -116,6 +119,12 @@ export namespace State {
TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
>);
error: undefined;
+ details: {
+ account: PaytoUri;
+ reserve: string;
+ username: string;
+ amount?: AmountJson;
+ };
id: string;
}
export interface Aborted {
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index 32d4fea7a..c2f79ac51 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -34,15 +34,18 @@ import { useSessionState } from "../../hooks/session.js";
import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { Props, State } from "./index.js";
+import { useSettingsContext } from "../../context/settings.js";
export function useComponentState({
currency,
routeClose,
onAbort,
+ focus,
routeHere,
onAuthorizationRequired,
}: Props): utils.RecursiveState<State> {
- const [settings] = usePreferences();
+ const [preference] = usePreferences();
+ const settings = useSettingsContext();
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
@@ -53,14 +56,14 @@ export function useComponentState({
const [failure, setFailure] = useState<
TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
>();
- const amount = settings.maxWithdrawalAmount;
+ const amount = settings.defaultSuggestedAmount;
async function doSilentStart() {
// FIXME: if amount is not enough use balance
const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`);
if (!creds) return;
const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
- settings.fastWithdrawal
+ preference.fastWithdrawalForm
? {
suggested_amount: Amounts.stringify(parsedAmount),
}
@@ -81,7 +84,7 @@ export function useComponentState({
if (withdrawalOperationId === undefined) {
doSilentStart();
}
- }, [settings.fastWithdrawal, amount]);
+ }, [preference.fastWithdrawalForm, amount]);
if (failure) {
return {
@@ -137,13 +140,19 @@ export function useComponentState({
return (): utils.RecursiveState<State> => {
const result = useWithdrawalDetails(withdrawalOperationId);
- const shouldCreateNewOperation = result && !(result instanceof TalerError);
+
+ const shouldCreateNewOperation =
+ result &&
+ (result instanceof TalerError ||
+ result.type === "fail" ||
+ result.body.status === "aborted" ||
+ result.body.status === "confirmed");
useEffect(() => {
if (shouldCreateNewOperation) {
doSilentStart();
}
- }, []);
+ }, [shouldCreateNewOperation]);
if (!result) {
return {
status: "loading",
@@ -182,7 +191,7 @@ export function useComponentState({
}
if (data.status === "confirmed") {
- if (!settings.showWithdrawalSuccess) {
+ if (!preference.showWithdrawalSuccess) {
updateBankState("currentWithdrawalOperationId", undefined);
// onClose()
}
@@ -199,6 +208,7 @@ export function useComponentState({
error: undefined,
uri: parsedUri,
routeClose,
+ focus,
onAbort: !creds
? async () => {
onAbort();
@@ -232,6 +242,12 @@ export function useComponentState({
status: "need-confirmation",
error: undefined,
routeHere,
+ details: {
+ account,
+ reserve: data.selected_reserve_pub,
+ username: data.username,
+ amount: !data.amount ? undefined : Amounts.parse(data.amount),
+ },
onAuthorizationRequired,
account: data.username,
id: withdrawalOperationId,
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx
index 62308eca6..2586ec01b 100644
--- a/packages/bank-ui/src/pages/OperationState/views.tsx
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -16,6 +16,7 @@
import {
AbsoluteTime,
+ Amounts,
HttpStatusCode,
TalerErrorCode,
TranslatedString,
@@ -26,6 +27,7 @@ import {
Attention,
LocalNotificationBanner,
notifyInfo,
+ useBankCoreApiContext,
useLocalNotification,
useTalerWalletIntegrationAPI,
useTranslationContext,
@@ -37,6 +39,7 @@ import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
import { State } from "./index.js";
+import { RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
export function InvalidPaytoView({ payto }: State.InvalidPayto) {
return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
@@ -53,6 +56,7 @@ export function NeedConfirmationView({
onConfirm: doConfirm,
routeHere,
account,
+ details,
id,
onAuthorizationRequired,
}: State.NeedConfirmation) {
@@ -60,6 +64,11 @@ export function NeedConfirmationView({
const [settings] = usePreferences();
const [notification, notify, errorHandler] = useLocalNotification();
const [, updateBankState] = useBankState();
+ const { config } = useBankCoreApiContext();
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
async function onCancel() {
errorHandler(async () => {
@@ -71,7 +80,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -79,7 +88,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -87,7 +96,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -112,7 +121,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -120,7 +129,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -128,7 +137,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -136,7 +145,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -144,7 +153,7 @@ export function NeedConfirmationView({
return notify({
type: "error",
title: i18n.str`Your balance is not enough.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -181,6 +190,164 @@ export function NeedConfirmationView({
e.preventDefault();
}}
>
+ <div class="px-4 mt-4">
+ <div class="w-full">
+ <dl class="">
+ {((): VNode => {
+ if (!details.account.isKnown) {
+ return (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.targetPath}
+ </dd>
+ </div>
+ );
+ }
+ switch (details.account.targetType) {
+ case "iban": {
+ const name = details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account number
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.iban}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "x-taler-bank": {
+ const name = details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account bank hostname
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.host}
+ </dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account id
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.account}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "bitcoin": {
+ const name = details.account.params["receiver-name"];
+ return (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account address
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.address}
+ </dd>
+ </div>
+ {name && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {name}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ default: {
+ assertUnreachable(details.account);
+ }
+ }
+ })()}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Amount</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.amount !== undefined ? (
+ <RenderAmount
+ value={details.amount}
+ spec={config.currency_specification}
+ />
+ ) : (
+ <i18n.Translate>
+ No amount specified yet.
+ </i18n.Translate>
+ )}
+ </dd>
+ </div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Cost</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ )}
+ </dl>
+ </div>
+ </div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<button
type="button"
@@ -221,7 +388,9 @@ export function FailedView({ error }: State.Failed) {
type="danger"
title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}
>
- <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ {!error.detail ? undefined :
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ }
</Attention>
);
case HttpStatusCode.Conflict:
@@ -230,7 +399,10 @@ export function FailedView({ error }: State.Failed) {
type="danger"
title={i18n.str`The operation was rejected due to insufficient funds.`}
>
- <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ {!error.detail ? undefined :
+
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ }
</Attention>
);
case HttpStatusCode.NotFound:
@@ -239,7 +411,9 @@ export function FailedView({ error }: State.Failed) {
type="danger"
title={i18n.str`The operation was rejected due to insufficient funds.`}
>
- <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ {!error.detail ? undefined :
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ }
</Attention>
);
default:
@@ -338,7 +512,11 @@ export function ConfirmedView({ routeClose }: State.Confirmed) {
);
}
-export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
+export function ReadyView({
+ uri,
+ focus,
+ onAbort: doAbort,
+}: State.Ready): VNode {
const { i18n } = useTranslationContext();
const walletInegrationApi = useTalerWalletIntegrationAPI();
const [notification, notify, errorHandler] = useLocalNotification();
@@ -357,7 +535,7 @@ export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
return notify({
type: "error",
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
- description: hasError.detail.hint as TranslatedString,
+ description: hasError.detail?.hint as TranslatedString,
debug: hasError.detail,
when: AbsoluteTime.now(),
});
@@ -365,7 +543,7 @@ export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
return notify({
type: "error",
title: i18n.str`The operation id is invalid.`,
- description: hasError.detail.hint as TranslatedString,
+ description: hasError.detail?.hint as TranslatedString,
debug: hasError.detail,
when: AbsoluteTime.now(),
});
@@ -373,7 +551,7 @@ export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
return notify({
type: "error",
title: i18n.str`The operation was not found.`,
- description: hasError.detail.hint as TranslatedString,
+ description: hasError.detail?.hint as TranslatedString,
debug: hasError.detail,
when: AbsoluteTime.now(),
});
@@ -387,60 +565,79 @@ export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
<Fragment>
<LocalNotificationBanner notification={notification} />
- <div class="flex justify-end mt-4">
- <button
- type="button"
- name="cancel"
- class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
- onClick={onAbort}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- </div>
-
- <div class="bg-white shadow sm:rounded-lg mt-4">
- <div class="p-4">
+ <div class="bg-white shadow-xl sm:rounded-lg">
+ <div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>On this device</i18n.Translate>
+ <i18n.Translate>
+ If you have a Taler wallet installed on this device
+ </i18n.Translate>
</h3>
- <div class="mt-2 sm:flex sm:items-start sm:justify-between">
- <div class="max-w-xl text-sm text-gray-500">
- <p>
- <i18n.Translate>
- If you are using a web browser on desktop you can also
- </i18n.Translate>
- </p>
- </div>
- <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
+ <div class="mt-4 mb-4 text-sm text-gray-500">
+ <p>
+ <i18n.Translate>
+ Your wallet will display the details of the transaction
+ including the fees (if applicable). If you do not yet have a
+ wallet, please follow the instructions
+ </i18n.Translate>{" "}
<a
- href={talerWithdrawUri}
- name="start"
- class="inline-flex items-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ class="font-semibold text-indigo-600 hover:text-indigo-900"
+ name="wallet page"
+ href="https://taler.net/en/wallet.html"
>
- <i18n.Translate>Start</i18n.Translate>
+ <i18n.Translate>on this page</i18n.Translate>
</a>
- </div>
+ .
+ </p>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 pt-2 mt-2 ">
+ <button
+ type="button"
+ name="cancel"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ // class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-black shadow-sm "
+ onClick={onAbort}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <a
+ href={talerWithdrawUri}
+ name="withdraw"
+ class="inline-flex items-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Withdraw</i18n.Translate>
+ </a>
</div>
</div>
</div>
- <div class="bg-white shadow sm:rounded-lg mt-2">
- <div class="p-4">
+
+ <div class="bg-white shadow-xl sm:rounded-lg mt-8">
+ <div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>On a mobile phone</i18n.Translate>
+ <i18n.Translate>
+ Or if you have the Taler wallet on another device
+ </i18n.Translate>
</h3>
- <div class="mt-2 sm:flex sm:items-start sm:justify-between">
- <div class="max-w-xl text-sm text-gray-500">
- <p>
- <i18n.Translate>
- Scan the QR code with your mobile device.
- </i18n.Translate>
- </p>
- </div>
+ <div class="mt-4 max-w-xl text-sm text-gray-500">
+ <i18n.Translate>
+ Scan the QR below to start the withdrawal.
+ </i18n.Translate>
</div>
<div class="mt-2 max-w-md ml-auto mr-auto">
<QR text={talerWithdrawUri} />
</div>
</div>
+ <div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button
+ type="button"
+ // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ // handler={onAbortHandler}
+ onClick={onAbort}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
</div>
</Fragment>
);
diff --git a/packages/bank-ui/src/pages/PaymentOptions.tsx b/packages/bank-ui/src/pages/PaymentOptions.tsx
index 386fe31bc..a31dd0714 100644
--- a/packages/bank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -41,29 +41,25 @@ function ShowOperationPendingTag({
const pending =
!loading &&
!error &&
- (result.body.status === "pending" || result.body.status === "selected") &&
+ result.body.status === "selected" &&
+ // (result.body.status === "pending" || result.body.status === "selected") &&
credentials.status === "loggedIn" &&
credentials.username === result.body.username;
- useEffect(() => {
- if (!loading && !pending && onOperationAlreadyCompleted) {
- onOperationAlreadyCompleted();
- }
- }, [pending]);
if (error || !pending) {
return <Fragment />;
}
return (
- <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700 whitespace-pre">
<svg
- class="h-1.5 w-1.5 fill-green-500"
+ class="h-1.5 w-1.5 fill-yellow-500"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx="3" cy="3" r="3" />
</svg>
- <i18n.Translate>Operation ready</i18n.Translate>
+ <i18n.Translate>Pending operation</i18n.Translate>
</span>
);
}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 90b41d331..9cba67384 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -79,6 +79,7 @@ export function PaytoWireTransferForm({
routeHere,
onAuthorizationRequired,
limit,
+ balance,
}: Props): VNode {
const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
const isRawPayto = inputType !== "form";
@@ -116,6 +117,11 @@ export function PaytoWireTransferForm({
? Amounts.zeroOfCurrency(config.currency)
: Amounts.parseOrThrow(config.wire_transfer_fees);
+ const limitWithFee =
+ Amounts.cmp(limit, wireFee) === 1
+ ? Amounts.sub(limit, wireFee).amount
+ : Amounts.zeroOfAmount(limit);
+
const errorsWire = undefinedIfEmpty({
account: !account
? i18n.str`Required`
@@ -129,7 +135,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsedAmount
? i18n.str`Not valid`
- : validateAmount(parsedAmount, limit, wireFee, i18n),
+ : validateAmount(parsedAmount, limitWithFee, i18n),
});
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
@@ -139,7 +145,7 @@ export function PaytoWireTransferForm({
? i18n.str`Required`
: !parsed
? i18n.str`Does not follow the pattern`
- : validateRawPayto(parsed, limit, wireFee, url.host, i18n, paytoType),
+ : validateRawPayto(parsed, limitWithFee, url.host, i18n, paytoType),
});
async function doSend() {
@@ -203,7 +209,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -211,7 +217,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`Not enough permission to complete the operation.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -219,7 +225,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`Bank administrator can't be the transfer creditor.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -229,7 +235,7 @@ export function PaytoWireTransferForm({
title: i18n.str`The destination account "${
acName ?? puri
}" was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -237,7 +243,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`The origin and the destination of the transfer can't be the same.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -245,7 +251,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`Your balance is not enough.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -253,7 +259,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`The origin account "${puri}" was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -261,7 +267,7 @@ export function PaytoWireTransferForm({
return notify({
type: "error",
title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -335,7 +341,9 @@ export function PaytoWireTransferForm({
setAmount(Amounts.stringifyValue(amount));
}
}
- const subject = parsed.params["message"];
+ const subject = !parsed.params["message"]
+ ? parsed.params["subject"]
+ : parsed.params["message"];
if (subject) {
setSubject(subject);
}
@@ -627,6 +635,17 @@ export function PaytoWireTransferForm({
</div>
</div>
)}
+ {Amounts.cmp(limitWithFee, balance) > 0 ? (
+ <p class="mt-2 text-sm text-gray-900">
+ <i18n.Translate>
+ You can transfer{" "}
+ <RenderAmount
+ value={limitWithFee}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ ) : undefined}
</div>
{Amounts.isZero(wireFee) ? undefined : (
<div class="px-4 my-4">
@@ -708,11 +727,9 @@ export function InputAmount(
currency,
name,
value,
- error,
left,
onChange,
}: {
- error?: string;
currency: string;
name: string;
left?: boolean | undefined;
@@ -760,11 +777,15 @@ export function InputAmount(
}}
/>
</div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
</div>
);
}
+/**
+ * send to web-utils
+ * @param param0
+ * @returns
+ */
export function RenderAmount({
value,
spec,
@@ -800,7 +821,6 @@ export function RenderAmount({
function validateRawPayto(
parsed: PaytoUri,
limit: AmountJson,
- fee: AmountJson,
host: string,
i18n: InternationalizationAPI,
type: "iban" | "x-taler-bank",
@@ -844,13 +864,15 @@ function validateRawPayto(
if (!amount) {
return i18n.str`The "amount" parameter is not valid`;
}
- result = validateAmount(amount, limit, fee, i18n);
+ result = validateAmount(amount, limit, i18n);
if (result) return result;
- if (!parsed.params.message) {
- return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`;
+ if (!parsed.params.message && !parsed.params.subject) {
+ return i18n.str`Missing the "message" or "subject" parameter to specify a reference text for the transfer`;
}
- const subject = parsed.params.message;
+ const subject = !parsed.params.message
+ ? parsed.params.subject
+ : parsed.params.message;
result = validateSubject(subject, i18n);
if (result) return result;
@@ -860,7 +882,6 @@ function validateRawPayto(
function validateAmount(
amount: AmountJson,
limit: AmountJson,
- fee: AmountJson,
i18n: InternationalizationAPI,
): TranslatedString | undefined {
if (amount.currency !== limit.currency) {
@@ -869,8 +890,7 @@ function validateAmount(
if (Amounts.isZero(amount)) {
return i18n.str`Can't transfer zero amount`;
}
- const amountWithFee = Amounts.add(amount, fee).amount;
- if (Amounts.cmp(limit, amountWithFee) === -1) {
+ if (Amounts.cmp(limit, amount) === -1) {
return i18n.str`Balance is not enough`;
}
return undefined;
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
index 2a21295c7..0d14f52d8 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -76,11 +76,12 @@ export function QrCodeSection({
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
+
<div class="bg-white shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>
- If you have a Taler wallet installed in this device
+ If you have a Taler wallet installed on this device
</i18n.Translate>
</h3>
<div class="mt-4 mb-4 text-sm text-gray-500">
@@ -88,19 +89,19 @@ export function QrCodeSection({
<i18n.Translate>
Your wallet will display the details of the transaction
including the fees (if applicable). If you do not yet have a
- wallet, please follow the instructions on
- </i18n.Translate>
+ wallet, please follow the instructions
+ </i18n.Translate>{" "}
<a
- class="font-semibold text-gray-500 hover:text-gray-400"
+ class="font-semibold text-indigo-600 hover:text-indigo-900"
name="wallet page"
href="https://taler.net/en/wallet.html"
>
- <i18n.Translate>this page</i18n.Translate>
+ <i18n.Translate>on this page</i18n.Translate>
</a>
.
</p>
</div>
- <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 ">
+ <div class="flex items-center justify-between gap-x-6 pt-2 mt-2 ">
<Button
type="button"
name="cancel"
@@ -124,7 +125,7 @@ export function QrCodeSection({
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>
- Or if you have the Taler wallet in another device
+ Or if you have the Taler wallet on another device
</i18n.Translate>
</h3>
<div class="mt-4 max-w-xl text-sm text-gray-500">
diff --git a/packages/bank-ui/src/pages/ShowNotifications.tsx b/packages/bank-ui/src/pages/ShowNotifications.tsx
index fe041fb19..5545b50c1 100644
--- a/packages/bank-ui/src/pages/ShowNotifications.tsx
+++ b/packages/bank-ui/src/pages/ShowNotifications.tsx
@@ -14,9 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useNotifications } from "@gnu-taler/web-util/browser";
+import { Time, useNotifications } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { Time } from "../components/Time.js";
export function ShowNotifications(): VNode {
const ns = useNotifications();
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
index 624890468..186038cad 100644
--- a/packages/bank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -30,7 +30,10 @@ import {
Attention,
Loading,
LocalNotificationBanner,
+ RouteDefinition,
ShowInputErrorLabel,
+ Time,
+ useBankCoreApiContext,
useLocalNotification,
useNavigationContext,
useTranslationContext,
@@ -38,17 +41,13 @@ import {
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
-import { Time } from "../components/Time.js";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useWithdrawalDetails } from "../hooks/account.js";
import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
import { useConversionInfo } from "../hooks/regional.js";
import { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { undefinedIfEmpty } from "../utils.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
const TAN_PREFIX = "T-";
const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/;
@@ -113,7 +112,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -121,7 +120,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -129,7 +128,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -155,7 +154,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`Challenge not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -163,7 +162,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`This user is not authorized to complete this challenge.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -171,7 +170,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`Too many attempts, try another code.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -179,7 +178,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`The confirmation code is wrong, try again.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -187,7 +186,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`The operation expired.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -206,7 +205,12 @@ export function SolveChallengePage({
case "update-password":
return await api.updatePassword(creds, ch.request, ch.id);
case "create-transaction":
- return await api.createTransaction(creds, ch.request, undefined, ch.id);
+ return await api.createTransaction(
+ creds,
+ ch.request,
+ undefined,
+ ch.id,
+ );
case "confirm-withdrawal":
return await api.confirmWithdrawalById(creds, ch.request, ch.id);
case "create-cashout":
@@ -221,7 +225,7 @@ export function SolveChallengePage({
return notify({
type: "error",
title: i18n.str`The operation failed.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -694,10 +698,14 @@ function ShowWithdrawalDetails({ id }: { id: string }): VNode {
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount
- value={Amounts.parseOrThrow(details.body.amount)}
- spec={config.currency_specification}
- />
+ {details.body.amount !== undefined ? (
+ <RenderAmount
+ value={Amounts.parseOrThrow(details.body.amount)}
+ spec={config.currency_specification}
+ />
+ ) : (
+ <i18n.Translate>No amount specified yet.</i18n.Translate>
+ )}
</dd>
</div>
{details.body.selected_reserve_pub !== undefined && (
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index 39dea018f..ec1ee1ff9 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -20,6 +20,7 @@ import {
Amounts,
HttpStatusCode,
TalerCorebankApi,
+ TalerError,
TranslatedString,
assertUnreachable,
parseWithdrawUri,
@@ -28,12 +29,13 @@ import {
Attention,
LocalNotificationBanner,
notifyError,
+ ShowInputErrorLabel,
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { forwardRef } from "preact/compat";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useSessionState } from "../hooks/session.js";
import { useBankState } from "../hooks/bank-state.js";
@@ -46,9 +48,47 @@ import {
RenderAmount,
doAutoFocus,
} from "./PaytoWireTransferForm.js";
+import { useSettingsContext } from "../context/settings.js";
+import { useWithdrawalDetails } from "../hooks/account.js";
const RefAmount = forwardRef(InputAmount);
+function ThereIsAnOperationWarning({ wopid, onClose, focus, routeOperationDetails }: {
+ focus?: boolean,
+ wopid: string,
+ onClose: () => void;
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
+
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const url = routeOperationDetails.url({ wopid });
+
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`There is an operation already`}
+ onClose={onClose}
+ >
+ <span ref={focus ? doAutoFocus : undefined} />
+ <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
+ <a
+ class="font-semibold text-yellow-700 hover:text-yellow-600"
+ name="complete operation"
+ href={url}
+ // onClick={(e) => {
+ // e.preventDefault()
+ // walletInegrationApi.publishTalerAction(uri, () => {
+ // navigateTo(url)
+ // })
+ // }}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ );
+
+}
+
function OldWithdrawalForm({
onOperationCreated,
limit,
@@ -65,7 +105,8 @@ function OldWithdrawalForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings] = usePreferences();
+ const settings = useSettingsContext();
+ const [preference] = usePreferences();
// const walletInegrationApi = useTalerWalletIntegrationAPI()
// const { navigateTo } = useNavigationContext();
@@ -80,11 +121,19 @@ function OldWithdrawalForm({
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
const [amountStr, setAmountStr] = useState<string | undefined>(
- `${settings.maxWithdrawalAmount}`,
+ `${settings.defaultSuggestedAmount ?? 1}`,
);
const [notification, notify, handleError] = useLocalNotification();
+ const result = useWithdrawalDetails(bankState.currentWithdrawalOperationId);
+ const loading = !result;
+ const error =
+ !loading && (result instanceof TalerError || result.type === "fail");
+ const pending =
+ !loading &&
+ !error &&
+ result.body.status === "pending";
- if (bankState.currentWithdrawalOperationId) {
+ if (pending) {
// FIXME: doing the preventDefault is not optimal
// const suri = stringifyWithdrawUri({
@@ -92,34 +141,43 @@ function OldWithdrawalForm({
// withdrawalOperationId: bankState.currentWithdrawalOperationId,
// });
// const uri = parseWithdrawUri(suri)!
- const url = routeOperationDetails.url({
- wopid: bankState.currentWithdrawalOperationId,
- });
- return (
- <Attention
- type="warning"
- title={i18n.str`There is an operation already`}
- onClose={() => {
- updateBankState("currentWithdrawalOperationId", undefined);
- }}
- >
- <span ref={focus ? doAutoFocus : undefined} />
- <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
- <a
- class="font-semibold text-yellow-700 hover:text-yellow-600"
- name="complete operation"
- href={url}
- // onClick={(e) => {
- // e.preventDefault()
- // walletInegrationApi.publishTalerAction(uri, () => {
- // navigateTo(url)
- // })
- // }}
- >
- <i18n.Translate>this page</i18n.Translate>
- </a>
- </Attention>
- );
+ return <ThereIsAnOperationWarning
+ onClose={() => {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ }}
+ routeOperationDetails={routeOperationDetails}
+ wopid={bankState.currentWithdrawalOperationId!}
+ focus
+
+ />
+ // const url = routeOperationDetails.url({
+ // wopid: bankState.currentWithdrawalOperationId,
+ // });
+ // return (
+ // <Attention
+ // type="warning"
+ // title={i18n.str`There is an operation already`}
+ // onClose={() => {
+ // updateBankState("currentWithdrawalOperationId", undefined);
+ // }}
+ // >
+ // <span ref={focus ? doAutoFocus : undefined} />
+ // <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
+ // <a
+ // class="font-semibold text-yellow-700 hover:text-yellow-600"
+ // name="complete operation"
+ // href={url}
+ // // onClick={(e) => {
+ // // e.preventDefault()
+ // // walletInegrationApi.publishTalerAction(uri, () => {
+ // // navigateTo(url)
+ // // })
+ // // }}
+ // >
+ // <i18n.Translate>this page</i18n.Translate>
+ // </a>
+ // </Attention>
+ // );
}
const trimmedAmountStr = amountStr?.trim();
@@ -143,13 +201,13 @@ function OldWithdrawalForm({
if (!parsedAmount || !creds) return;
await handleError(async () => {
const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
- settings.fastWithdrawal
+ preference.fastWithdrawalForm
? {
- suggested_amount: Amounts.stringify(parsedAmount),
- }
+ suggested_amount: Amounts.stringify(parsedAmount),
+ }
: {
- amount: Amounts.stringify(parsedAmount),
- };
+ amount: Amounts.stringify(parsedAmount),
+ };
const resp = await api.createWithdrawal(creds, params);
if (resp.type === "ok") {
const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
@@ -171,7 +229,7 @@ function OldWithdrawalForm({
notify({
type: "error",
title: i18n.str`The operation was rejected due to insufficient funds`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -181,7 +239,7 @@ function OldWithdrawalForm({
notify({
type: "error",
title: i18n.str`The operation was rejected due to insufficient funds`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -191,7 +249,7 @@ function OldWithdrawalForm({
notify({
type: "error",
title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -226,10 +284,10 @@ function OldWithdrawalForm({
onChange={(v) => {
setAmountStr(v);
}}
- error={errors?.amount}
ref={focus ? doAutoFocus : undefined}
/>
</div>
+ <ShowInputErrorLabel message={errors?.amount} isDirty={amountStr !== undefined} />
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
@@ -241,9 +299,9 @@ function OldWithdrawalForm({
</i18n.Translate>
</p>
{Amounts.cmp(limit, balance) > 0 ? (
- <p class="mt-2 text-sm text-gray-500">
+ <p class="mt-2 text-sm text-gray-900">
<i18n.Translate>
- Your account allows you to withdraw{" "}
+ You can withdraw up to{" "}
<RenderAmount
value={limit}
spec={config.currency_specification}
@@ -347,28 +405,28 @@ export function WalletWithdrawForm({
routeCancel: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = usePreferences();
+ const [pref, updatePref] = usePreferences();
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Prepare your Taler wallet</i18n.Translate>
+ <i18n.Translate>Use your Taler wallet</i18n.Translate>
</h2>
<p class="mt-1 text-sm text-gray-500">
<i18n.Translate>
- After using your wallet you will need to confirm or cancel the
+ After using your wallet you will need to authorize or cancel the
operation on this site.
</i18n.Translate>
</p>
</div>
<div class="col-span-2">
- {settings.showInstallWallet && (
+ {pref.showInstallWallet && (
<Attention
title={i18n.str`You need a Taler wallet`}
onClose={() => {
- updateSettings("showInstallWallet", false);
+ updatePref("showInstallWallet", false);
}}
>
<i18n.Translate>
@@ -386,7 +444,7 @@ export function WalletWithdrawForm({
</Attention>
)}
- {!settings.fastWithdrawal ? (
+ {!pref.fastWithdrawalForm ? (
<OldWithdrawalForm
focus={focus}
routeOperationDetails={routeOperationDetails}
@@ -397,12 +455,13 @@ export function WalletWithdrawForm({
/>
) : (
<OperationState
+ focus={focus}
currency={limit.currency}
onAuthorizationRequired={onAuthorizationRequired}
routeClose={routeCancel}
routeHere={routeOperationDetails}
onAbort={onOperationAborted}
- // route={routeCancel}
+ // route={routeCancel}
/>
)}
</div>
diff --git a/packages/bank-ui/src/pages/WireTransfer.tsx b/packages/bank-ui/src/pages/WireTransfer.tsx
index f45390938..817145702 100644
--- a/packages/bank-ui/src/pages/WireTransfer.tsx
+++ b/packages/bank-ui/src/pages/WireTransfer.tsx
@@ -62,7 +62,12 @@ export function WireTransfer({
return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
+ return (
+ <Fragment>
+ <ErrorLoadingWithDebug error={result} />
+ <LoginForm currentUser={account} />
+ </Fragment>
+ );
}
if (result.type === "fail") {
switch (result.case) {
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 853dd7bae..efbc1bc83 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -17,6 +17,7 @@
import {
AbsoluteTime,
AmountJson,
+ Amounts,
HttpStatusCode,
PaytoUri,
PaytoUriIBAN,
@@ -51,7 +52,7 @@ interface Props {
account: PaytoUri;
reserve: string;
username: string;
- amount: AmountJson;
+ amount?: AmountJson;
};
onAuthorizationRequired: () => void;
}
@@ -79,6 +80,11 @@ export function WithdrawalConfirmationQuestion({
lib: { bank: api },
} = useBankCoreApiContext();
+ const wireFee =
+ config.wire_transfer_fees === undefined
+ ? Amounts.zeroOfCurrency(config.currency)
+ : Amounts.parseOrThrow(config.wire_transfer_fees);
+
async function doTransfer() {
await handleError(async () => {
if (!creds) return;
@@ -97,7 +103,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -105,7 +111,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -113,7 +119,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -121,7 +127,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -129,7 +135,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`Your balance is not enough for the operation.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -167,7 +173,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -175,7 +181,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -183,7 +189,7 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -351,12 +357,35 @@ export function WithdrawalConfirmationQuestion({
<i18n.Translate>Amount</i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount
- value={details.amount}
- spec={config.currency_specification}
- />
+ {details.amount !== undefined ? (
+ <RenderAmount
+ value={details.amount}
+ spec={config.currency_specification}
+ />
+ ) : (
+ <i18n.Translate>
+ No amount specified yet.
+ </i18n.Translate>
+ )}
</dd>
</div>
+ {Amounts.isZero(wireFee) ? undefined : (
+ <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Cost</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={wireFee}
+ negative
+ withColor
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ )}
</dl>
</div>
</div>
diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
index c0c55f14b..832478656 100644
--- a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -27,11 +27,12 @@ export function WithdrawalOperationPage({
onAuthorizationRequired,
onOperationAborted,
routeClose,
+ origin,
routeWithdrawalDetails,
}: {
onAuthorizationRequired: () => void;
operationId: string;
- purpose: "after-creation" | "after-confirmation";
+ origin: "from-bank-ui" | "from-wallet-ui";
onOperationAborted: () => void;
routeClose: RouteDefinition;
routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
@@ -61,6 +62,7 @@ export function WithdrawalOperationPage({
return (
<WithdrawalQRCode
withdrawUri={parsedUri}
+ origin={origin}
routeWithdrawalDetails={routeWithdrawalDetails}
onAuthorizationRequired={onAuthorizationRequired}
onOperationAborted={() => {
diff --git a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
index b61f0cc8f..37918396a 100644
--- a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
@@ -21,6 +21,7 @@ import {
WithdrawUriResult,
assertUnreachable,
parsePaytoUri,
+ stringifyWithdrawUri,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -37,6 +38,7 @@ import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion
interface Props {
withdrawUri: WithdrawUriResult;
+ origin: "from-bank-ui" | "from-wallet-ui";
onOperationAborted: () => void;
routeClose: RouteDefinition;
routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
@@ -51,6 +53,7 @@ export function WithdrawalQRCode({
withdrawUri,
onOperationAborted,
routeClose,
+ origin,
routeWithdrawalDetails,
onAuthorizationRequired,
}: Props): VNode {
@@ -122,6 +125,7 @@ export function WithdrawalQRCode({
</div>
);
}
+ const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
if (data.status === "confirmed") {
return (
@@ -161,14 +165,23 @@ export function WithdrawalQRCode({
</div>
</div>
</div>
- <div class="mt-5 sm:mt-6">
+ <div class="mt-5 sm:mt-6 items-center justify-between gap-x-2 flex">
<a
href={routeClose.url({})}
name="done"
- class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ class="inline-flex justify-center rounded-md bg-white-600 px-3 py-2 text-sm font-semibold text-black shadow-sm "
>
- <i18n.Translate>Done</i18n.Translate>
+ <i18n.Translate>Close</i18n.Translate>
</a>
+ {origin === "from-wallet-ui" ? (
+ <a
+ href={talerWithdrawUri}
+ name="done"
+ class="inline-flex justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Go to wallet</i18n.Translate>
+ </a>
+ ) : undefined}
</div>
</div>
);
@@ -239,7 +252,7 @@ export function WithdrawalQRCode({
username: data.username,
account,
reserve: data.selected_reserve_pub,
- amount: Amounts.parseOrThrow(data.amount),
+ amount: !data.amount ? undefined : Amounts.parseOrThrow(data.amount),
}}
onAuthorizationRequired={onAuthorizationRequired}
onAborted={() => {
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index 0e2144d77..b785f582b 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -22,9 +22,10 @@ import {
TalerErrorCode,
TranslatedString,
assertUnreachable,
- parsePaytoUri
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
+ Attention,
CopyButton,
Loading,
LocalNotificationBanner,
@@ -43,6 +44,7 @@ import { useSessionState } from "../../hooks/session.js";
import { LoginForm } from "../LoginForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
import { AccountForm } from "../admin/AccountForm.js";
+import { usePreferences } from "../../hooks/preferences.js";
export function ShowAccountDetails({
account,
@@ -68,6 +70,7 @@ export function ShowAccountDetails({
account: string;
}): VNode {
const { i18n } = useTranslationContext();
+ const [preferences] = usePreferences();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
const {
@@ -89,7 +92,12 @@ export function ShowAccountDetails({
return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
+ return (
+ <Fragment>
+ <ErrorLoadingWithDebug error={result} />
+ <LoginForm currentUser={account} />
+ </Fragment>
+ );
}
if (result.type === "fail") {
switch (result.case) {
@@ -121,7 +129,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`The rights to change the account are not sufficient`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -129,7 +137,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`The username was not found`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -137,7 +145,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`You can't change the legal name, please contact the your account administrator.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -145,7 +153,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`You can't change the debt limit, please contact the your account administrator.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -153,7 +161,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`You can't change the cashout address, please contact the your account administrator.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -161,7 +169,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`No information for the selected authentication channel.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -179,7 +187,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`Authentication channel is not supported.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -188,7 +196,7 @@ export function ShowAccountDetails({
return notify({
type: "error",
title: i18n.str`Only the administrator can change the minimum cashout limit.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -202,16 +210,18 @@ export function ShowAccountDetails({
const url = bank.getRevenueAPI(account);
const baseURL = url.href;
- const revenueURL = new URL(baseURL)
+ const revenueURL = new URL(baseURL);
revenueURL.username = account;
- revenueURL.password = creds?.token ?? ""
+ revenueURL.password;
const ac = parsePaytoUri(result.body.payto_uri);
const payto = !ac?.isKnown ? undefined : ac;
- const accountLetter : AccountLetter | undefined = !payto
+ const accountLetter: AccountLetter | undefined = !payto
? undefined
: {
- accountURI: result.body.payto_uri, infoURL: revenueURL.href
- }
+ accountURI: result.body.payto_uri,
+ infoURL: revenueURL.href,
+ accountToken: creds?.token,
+ };
return (
<Fragment>
@@ -231,6 +241,12 @@ export function ShowAccountDetails({
</h1>
)}
+ {result.body.status !== "deleted" ? undefined : (
+ <Attention title={i18n.str`Removed`} type="info">
+ <i18n.Translate>This account can't be used.</i18n.Translate>
+ </Attention>
+ )}
+
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
@@ -339,15 +355,21 @@ export function ShowAccountDetails({
{i18n.str`IBAN`}
</label>
<div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={payto.iban}
- autocomplete="off"
- />
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={payto.iban}
+ autocomplete="off"
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => payto.iban}
+ />
+ </div>
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
@@ -359,30 +381,70 @@ export function ShowAccountDetails({
}
case "x-taler-bank": {
return (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="account-name"
- >
- {i18n.str`Account name`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="account-name"
- id="account-name"
- disabled={true}
- value={payto.account}
- autocomplete="off"
- />
+ <Fragment>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-host"
+ >
+ {i18n.str`Account name`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-host"
+ id="account-host"
+ disabled={true}
+ value={payto.host}
+ autocomplete="off"
+ />
+ </div>
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => payto.host}
+ />
+ </div>
+
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Bank host where the service is located.
+ </i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- Bank account identifier for wire transfers.
- </i18n.Translate>
- </p>
- </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-name"
+ >
+ {i18n.str`Account name`}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="account-name"
+ id="account-name"
+ disabled={true}
+ value={payto.account}
+ autocomplete="off"
+ />
+ </div>
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => payto.account}
+ />
+ </div>
+
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Bank account identifier for wire transfers.
+ </i18n.Translate>
+ </p>
+ </div>
+ </Fragment>
);
}
case "bitcoin": {
@@ -401,9 +463,13 @@ export function ShowAccountDetails({
name="iban"
id="iban"
disabled={true}
- value={"DE1231231231"}
+ value={"asd"}
autocomplete="off"
/>
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => "Asd"}
+ />
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
@@ -424,15 +490,21 @@ export function ShowAccountDetails({
{i18n.str`Owner's name`}
</label>
<div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={result.body.name}
- autocomplete="off"
- />
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={result.body.name}
+ autocomplete="off"
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => result.body.name}
+ />
+ </div>
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
@@ -448,15 +520,21 @@ export function ShowAccountDetails({
{i18n.str`Account info URL`}
</label>
<div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={baseURL}
- autocomplete="off"
- />
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={baseURL}
+ autocomplete="off"
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => baseURL}
+ />
+ </div>
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
@@ -475,12 +553,20 @@ export function ShowAccountDetails({
>
<i18n.Translate>Cancel</i18n.Translate>
</a>
- <CopyButton
- getContent={() => !accountLetter ? "" : JSON.stringify(accountLetter)}
- class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Copy</i18n.Translate>
- </CopyButton>
+ <span></span>
+
+ {!preferences.showCopyAccount ? (
+ <span />
+ ) : (
+ <CopyButton
+ getContent={() =>
+ !accountLetter ? "" : JSON.stringify(accountLetter)
+ }
+ class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Copy</i18n.Translate>
+ </CopyButton>
+ )}
</div>
</div>
)}
@@ -488,4 +574,3 @@ export function ShowAccountDetails({
</Fragment>
);
}
-
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
index 2724fba11..2ffe7ad81 100644
--- a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -115,7 +115,7 @@ export function UpdateAccountPassword({
return notify({
type: "error",
title: i18n.str`Not authorized to change the password, maybe the session is invalid.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -123,7 +123,7 @@ export function UpdateAccountPassword({
return notify({
type: "error",
title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -131,7 +131,7 @@ export function UpdateAccountPassword({
return notify({
type: "error",
title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -139,7 +139,7 @@ export function UpdateAccountPassword({
return notify({
type: "error",
title: i18n.str`Your current password doesn't match, can't change to a new password.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
index ba5da609f..79064770e 100644
--- a/packages/bank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -26,11 +26,11 @@ import {
import {
CopyButton,
ShowInputErrorLabel,
+ useBankCoreApiContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useSessionState } from "../../hooks/session.js";
import {
ErrorMessageMappingFor,
@@ -110,7 +110,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const defaultValue: AccountFormData = {
debit_threshold: Amounts.stringifyValue(
- template?.debit_threshold ?? config.default_debit_threshold,
+ template?.debit_threshold ??
+ config.default_debit_threshold ??
+ `${config.currency}:0`,
),
min_cashout: Amounts.stringifyValue(
template?.min_cashout ?? `${config.currency}:0`,
@@ -213,10 +215,11 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
: undefined,
name: !editableName
? undefined // disabled
- : purpose === "update" && newForm.name === undefined ? undefined // the field hasn't been changed
- : !newForm.name
- ? i18n.str`Required`
- : undefined,
+ : purpose === "update" && newForm.name === undefined
+ ? undefined // the field hasn't been changed
+ : !newForm.name
+ ? i18n.str`Required`
+ : undefined,
username: !editableUsername
? undefined
: !newForm.username
diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx
index 6402c2bcd..83284c032 100644
--- a/packages/bank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -121,7 +121,11 @@ export function AccountList({
item.balance.credit_debit_indicator == "debit";
return (
- <tr key={idx}>
+ <tr
+ key={idx}
+ class="data-[status=deleted]:bg-gray-100"
+ data-status={item.status}
+ >
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
<a
name={`show account ${item.username}`}
@@ -159,42 +163,36 @@ export function AccountList({
)}
</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
- <a
- name={`update password ${item.username}`}
- href={routeUpdatePasswordAccount.url({
- account: item.username,
- })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- <i18n.Translate>Change password</i18n.Translate>
- </a>
- <br />
- {/* {config.allow_conversion ?
+ {item.status === "deleted" ? (
+ <p class="text-gray-600">removed</p>
+ ) : (
<Fragment>
-
<a
- name={`show cashout ${item.username}`}
- href={routeShowCashoutsAccount.url({
+ name={`update password ${item.username}`}
+ href={routeUpdatePasswordAccount.url({
account: item.username,
})}
class="text-indigo-600 hover:text-indigo-900"
>
- <i18n.Translate>Cashouts</i18n.Translate>
+ <i18n.Translate>
+ Change password
+ </i18n.Translate>
</a>
<br />
+
+ {noBalance ? (
+ <a
+ name={`remove account ${item.username}`}
+ href={routeRemoveAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>Remove</i18n.Translate>
+ </a>
+ ) : undefined}
</Fragment>
- : undefined} */}
- {noBalance ? (
- <a
- name={`remove account ${item.username}`}
- href={routeRemoveAccount.url({
- account: item.username,
- })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- <i18n.Translate>Remove</i18n.Translate>
- </a>
- ) : undefined}
+ )}
</td>
</tr>
);
diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
index 68f39fb9f..ba08f03cc 100644
--- a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -70,7 +70,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Server replied that phone or email is invalid`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -78,7 +78,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`The rights to perform the operation are not sufficient`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -86,7 +86,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Account username is already taken`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -94,7 +94,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Account id is already taken`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -102,7 +102,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Bank ran out of bonus credit.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -110,7 +110,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Account username can't be used because is reserved`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -118,7 +118,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Only admin is allow to set debt limit.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -126,7 +126,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`No information for the selected authentication channel.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -134,7 +134,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Authentication channel is not supported.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -142,7 +142,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Only admin can create accounts with second factor authentication.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -150,7 +150,7 @@ export function CreateNewAccount({
return notify({
type: "error",
title: i18n.str`Only the administrator can change the minimum cashout limit.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
index 8f6bb7c23..40e5ed884 100644
--- a/packages/bank-ui/src/pages/admin/DownloadStats.tsx
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -468,7 +468,7 @@ async function fetchAllStatus(
: undefined;
if (previous && previous.type === "fail" && options.endOnFirstFail) {
- throw TalerError.fromUncheckedDetail(previous.detail);
+ return accumulatedMap; //skip
}
const current = await api.getMonitor(token, {
@@ -477,7 +477,7 @@ async function fetchAllStatus(
});
if (current.type === "fail" && options.endOnFirstFail) {
- throw TalerError.fromUncheckedDetail(current.detail);
+ return accumulatedMap; //skip
}
const metricName =
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
index dbeebf719..bd715bcc9 100644
--- a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -74,7 +74,12 @@ export function RemoveAccount({
return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
+ return (
+ <Fragment>
+ <ErrorLoadingWithDebug error={result} />
+ <LoginForm currentUser={account} />
+ </Fragment>
+ );
}
if (result.type === "fail") {
switch (result.case) {
@@ -127,7 +132,7 @@ export function RemoveAccount({
return notify({
type: "error",
title: i18n.str`No enough permission to delete the account.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -135,7 +140,7 @@ export function RemoveAccount({
return notify({
type: "error",
title: i18n.str`The username was not found.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -143,7 +148,7 @@ export function RemoveAccount({
return notify({
type: "error",
title: i18n.str`Can't delete a reserved username.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -151,7 +156,7 @@ export function RemoveAccount({
return notify({
type: "error",
title: i18n.str`Can't delete an account with balance different than zero.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
index 485ef5490..ea2619cea 100644
--- a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -205,7 +205,7 @@ function useComponentState({
return notify({
type: "error",
title: i18n.str`Wrong credentials`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -214,7 +214,7 @@ function useComponentState({
return notify({
type: "error",
title: i18n.str`Conversion is disabled`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -612,60 +612,60 @@ function createFormValidator(
const errors = undefinedIfEmpty<FormErrors<FormType>>({
conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
cashin_min_amount: !state.conv.cashin_min_amount
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashin_min_amount
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashin_tiny_amount: !state.conv.cashin_tiny_amount
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashin_tiny_amount
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashin_fee: !state.conv.cashin_fee
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashin_fee
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashout_min_amount: !state.conv.cashout_min_amount
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashout_min_amount
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashout_tiny_amount: !state.conv.cashin_tiny_amount
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashout_tiny_amount
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashout_fee: !state.conv.cashin_fee
- ? i18n.str`required`
+ ? i18n.str`Required`
: !cashout_fee
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashin_rounding_mode: !state.conv.cashin_rounding_mode
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
cashout_rounding_mode: !state.conv.cashout_rounding_mode
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
cashin_ratio: !state.conv.cashin_ratio
- ? i18n.str`required`
+ ? i18n.str`Required`
: Number.isNaN(cashin_ratio)
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
cashout_ratio: !state.conv.cashout_ratio
- ? i18n.str`required`
+ ? i18n.str`Required`
: Number.isNaN(cashout_ratio)
- ? i18n.str`invalid`
+ ? i18n.str`Rnvalid`
: undefined,
}),
amount: !state.amount
- ? i18n.str`required`
+ ? i18n.str`Required`
: !am
- ? i18n.str`invalid`
+ ? i18n.str`Invalid`
: undefined,
});
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
index c51b96b8b..da3a03287 100644
--- a/packages/bank-ui/src/pages/regional/CreateCashout.tsx
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -29,15 +29,16 @@ import {
Attention,
Loading,
LocalNotificationBanner,
+ RouteDefinition,
ShowInputErrorLabel,
notifyInfo,
+ useBankCoreApiContext,
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useAccountDetails } from "../../hooks/account.js";
import { useBankState } from "../../hooks/bank-state.js";
import {
@@ -46,7 +47,6 @@ import {
useConversionInfo,
} from "../../hooks/regional.js";
import { useSessionState } from "../../hooks/session.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { TanChannel, undefinedIfEmpty } from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
import {
@@ -182,7 +182,10 @@ export function CreateCashout({
balanceIsDebit:
resultAccount.body.balance.credit_debit_indicator == "debit",
debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
- minCashout: resultAccount.body.min_cashout === undefined ? regionalZero : Amounts.parseOrThrow(resultAccount.body.min_cashout)
+ minCashout:
+ resultAccount.body.min_cashout === undefined
+ ? regionalZero
+ : Amounts.parseOrThrow(resultAccount.body.min_cashout),
};
const limit = account.balanceIsDebit
@@ -245,25 +248,24 @@ export function CreateCashout({
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`Balance is not enough`
: calculationResult === "amount-is-too-small"
- ? i18n.str`Amount needs to be higher`
- : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0
- ? i18n.str`No account can't cashout less than ${
- Amounts.stringifyValueWithSpec(
- Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
- regional_currency_specification,
- ).normal
- }`
- : Amounts.cmp(calc.debit, account.minCashout) < 0
- ? i18n.str`Your account can't cashout less than ${
+ ? i18n.str`Amount needs to be higher`
+ : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0
+ ? i18n.str`No account can't cashout less than ${
Amounts.stringifyValueWithSpec(
- Amounts.parseOrThrow(account.minCashout),
+ Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
regional_currency_specification,
).normal
}`
-
- : Amounts.isZero(calc.credit)
- ? i18n.str`The total transfer at destination will be zero`
- : undefined,
+ : Amounts.cmp(calc.debit, account.minCashout) < 0
+ ? i18n.str`Your account can't cashout less than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(account.minCashout),
+ regional_currency_specification,
+ ).normal
+ }`
+ : Amounts.isZero(calc.credit)
+ ? i18n.str`The total transfer at destination will be zero`
+ : undefined,
});
const trimmedAmountStr = form.amount?.trim();
@@ -297,7 +299,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -305,7 +307,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`Duplicated request detected, check if the operation succeeded or try again.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -313,7 +315,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`The conversion rate was incorrectly applied`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -321,7 +323,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`The account does not have sufficient funds`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -329,7 +331,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`Cashout are disabled`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -337,7 +339,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`Missing cashout URI in the profile`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -345,7 +347,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`The amount is less than the minimum allowed.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -354,7 +356,7 @@ export function CreateCashout({
return notify({
type: "error",
title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
- description: resp.detail.hint as TranslatedString,
+ description: resp.detail?.hint as TranslatedString ,
debug: resp.detail,
when: AbsoluteTime.now(),
});
@@ -364,7 +366,7 @@ export function CreateCashout({
});
}
const cashoutDisabled =
- config.supported_tan_channels.length < 1 ||
+ (config.supported_tan_channels ?? []).length < 1 ||
!resultAccount.body.cashout_payto_uri;
const cashoutAccount = !resultAccount.body.cashout_payto_uri
diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
index aba00ad7a..09efc021f 100644
--- a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -23,13 +23,13 @@ import {
import {
Attention,
Loading,
+ RouteDefinition,
+ Time,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { Time } from "../../components/Time.js";
import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { RenderAmount } from "../PaytoWireTransferForm.js";
interface Props {
diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json
index df5fe75ce..f14168e77 100644
--- a/packages/bank-ui/src/settings.json
+++ b/packages/bank-ui/src/settings.json
@@ -2,6 +2,8 @@
"backendBaseURL": "http://bank.taler.test:1180/",
"simplePasswordForRandomAccounts": true,
"allowRandomAccountCreation": true,
+ "fastWithdrawalForm": true,
+ "defaultSuggestedAmount": 11,
"bankName": "Taler DEVELOPMENT Bank",
"topNavSites": {
"Exchange": "http://Exchnage.taler.test:1180/",
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
index c085c7cd8..c1e418bc1 100644
--- a/packages/bank-ui/src/settings.ts
+++ b/packages/bank-ui/src/settings.ts
@@ -20,6 +20,7 @@ import {
canonicalizeBaseUrl,
codecForBoolean,
codecForMap,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -45,6 +46,10 @@ export interface UiSettings {
// - value: link target, where the user is going to be redirected
// default: empty list
topNavSites?: Record<string, string>;
+ // When the withdrawal form use the suggested amount the bank
+ // will send a default value that the user can change.
+ // default: 10
+ defaultSuggestedAmount?: number;
}
/**
@@ -56,12 +61,14 @@ const defaultSettings: UiSettings = {
simplePasswordForRandomAccounts: false,
allowRandomAccountCreation: false,
topNavSites: {},
+ defaultSuggestedAmount: 10,
};
const codecForUISettings = (): Codec<UiSettings> =>
buildCodecForObject<UiSettings>()
.property("backendBaseURL", codecOptional(codecForString()))
.property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
+ .property("defaultSuggestedAmount", codecOptional(codecForNumber()))
.property(
"simplePasswordForRandomAccounts",
codecOptional(codecForBoolean()),
diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts
index 921f9f9ea..81c27e6e0 100644
--- a/packages/bank-ui/src/stories.test.ts
+++ b/packages/bank-ui/src/stories.test.ts
@@ -20,7 +20,7 @@
*/
import {
AmountString,
- TalerCorebankApi,
+ TalerCorebankConfigResponse,
setupI18n,
} from "@gnu-taler/taler-util";
import {
@@ -54,7 +54,7 @@ describe("All the examples:", () => {
});
function DefaultTestingContext(_props: { children: ComponentChildren }): VNode {
- const cfg: TalerCorebankApi.Config = {
+ const cfg: TalerCorebankConfigResponse = {
name: "libeufin-bank",
allow_deletions: true,
bank_name: "taler bank",
diff --git a/packages/auditor-backoffice-ui/src/sw.js b/packages/bank-ui/src/type-override.d.ts
index bf52db6fa..703b60331 100644
--- a/packages/auditor-backoffice-ui/src/sw.js
+++ b/packages/bank-ui/src/type-override.d.ts
@@ -15,11 +15,18 @@
*/
/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
+ * define unknown type of catch function
*/
-
-// import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
-
-// setupRouting();
-// setupPrecaching(getFiles());
+interface Promise<T> {
+ /**
+ * Attaches a callback for only the rejection of the Promise.
+ * @param onrejected The callback to execute when the Promise is rejected.
+ * @returns A Promise for the completion of the callback.
+ */
+ catch<TResult = never>(
+ onrejected?:
+ | ((reason: unknown) => TResult | PromiseLike<TResult>)
+ | undefined
+ | null,
+ ): Promise<T | TResult>;
+}
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
index 2cc502416..64413b4d6 100644
--- a/packages/bank-ui/src/utils.ts
+++ b/packages/bank-ui/src/utils.ts
@@ -134,7 +134,7 @@ export async function withRuntimeErrorHandling<T>(
): Promise<void> {
try {
await cb();
- } catch (error: unknown) {
+ } catch (error) {
if (error instanceof TalerError) {
notify(buildRequestErrorMessage(i18n, error));
} else {
diff --git a/packages/challenger-ui/build.mjs b/packages/challenger-ui/build.mjs
index 166647f79..2e116d214 100755
--- a/packages/challenger-ui/build.mjs
+++ b/packages/challenger-ui/build.mjs
@@ -25,16 +25,6 @@ await build({
base: "src",
files: [
"src/index.html",
- "src/attempts-exhausted.html",
- "src/enter-address-form.html",
- "src/enter-email-form.html",
- "src/enter-file-access-form.html",
- "src/enter-phone-form.html",
- "src/enter-tan-form.html",
- "src/internal-error.html",
- "src/invalid-pin.html",
- "src/invalid-request.html",
- "src/validation-unknown.html",
]
}],
},
diff --git a/packages/challenger-ui/create_must.sh b/packages/challenger-ui/create_must.sh
deleted file mode 100755
index a4d78b2cc..000000000
--- a/packages/challenger-ui/create_must.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-# This file is in the public domain.
-
-# After the compilation succeeded
-# some changes needs to be made
-# in the html/js files to match the
-# what the service expects
-
-cd dist/prod
-
-for file in *.html; do
-
- # 1. remove the js reference used for dev
- sed /main.js/d -i $file
-
- # 2. change the location of css since
- #challenger backend wants them in the root path
- sed 's/="main.css"/="..\/main.css"/' -i $file
-
- # 3. rename the extension to must template
- mv $file ${file:0:-5}.en.must
-done
-
-#delete unused files
-rm *.js *.map
diff --git a/packages/challenger-ui/dev.mjs b/packages/challenger-ui/dev.mjs
index 595c3e99e..a6b715538 100755
--- a/packages/challenger-ui/dev.mjs
+++ b/packages/challenger-ui/dev.mjs
@@ -28,16 +28,6 @@ const build = initializeDev({
base: "src",
files: [
"src/index.html",
- "src/attempts-exhausted.html",
- "src/enter-address-form.html",
- "src/enter-email-form.html",
- "src/enter-file-access-form.html",
- "src/enter-phone-form.html",
- "src/enter-tan-form.html",
- "src/internal-error.html",
- "src/invalid-pin.html",
- "src/invalid-request.html",
- "src/validation-unknown.html",
]
}],
},
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
index 7cc73771b..383d44375 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,13 +1,12 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.11.4",
+ "version": "0.13.4",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
"type": "module",
"scripts": {
- "build": "./build.mjs && ./create_must.sh",
"check": "tsc",
"compile": "tsc && ./build.mjs",
"test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
@@ -59,9 +58,6 @@
"swr": "2.0.3",
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
- "date-fns": "2.29.3",
- "jed": "1.1.1",
- "qrcode-generator": "^1.4.4",
"preact": "10.11.3"
}
}
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
index 6166f159a..e2a4bd067 100644
--- a/packages/challenger-ui/src/Routing.tsx
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -15,22 +15,20 @@
*/
import {
- Loading,
urlPattern,
useCurrentLocation,
- useNavigationContext,
+ useNavigationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { assertUnreachable } from "@gnu-taler/taler-util";
+import { useErrorBoundary } from "preact/hooks";
import { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
-import { SessionId, useSessionState } from "./hooks/session.js";
+import { SessionId } from "./hooks/session.js";
import { AnswerChallenge } from "./pages/AnswerChallenge.js";
import { AskChallenge } from "./pages/AskChallenge.js";
import { CallengeCompleted } from "./pages/CallengeCompleted.js";
import { Frame } from "./pages/Frame.js";
-import { MissingParams } from "./pages/MissingParams.js";
-import { NonceNotFound } from "./pages/NonceNotFound.js";
import { Setup } from "./pages/Setup.js";
export function Routing(): VNode {
@@ -44,26 +42,11 @@ export function Routing(): VNode {
}
const publicPages = {
- noinfo: urlPattern<{ nonce: string }>(
- /\/noinfo\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/noinfo/${nonce}`,
- ),
- authorize: urlPattern<{ nonce: string }>(
- /\/authorize\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/authorize/${nonce}`,
- ),
- ask: urlPattern<{ nonce: string }>(
- /\/ask\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/ask/${nonce}`,
- ),
- answer: urlPattern<{ nonce: string }>(
- /\/answer\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/answer/${nonce}`,
- ),
- completed: urlPattern<{ nonce: string }>(
- /\/completed\/(?<nonce>[a-zA-Z0-9]+)/,
- ({ nonce }) => `#/completed/${nonce}`,
- ),
+ noinfo: urlPattern(/\/noinfo/, () => `#/noinfo`),
+ authorize: urlPattern(/\/authorize/, () => `#/authorize`),
+ ask: urlPattern(/\/ask/, () => `#/ask`),
+ answer: urlPattern(/\/answer/, () => `#/answer`),
+ completed: urlPattern(/\/completed/, () => `#/completed`),
setup: urlPattern<{ client: string }>(
/\/setup\/(?<client>[0-9]+)/,
({ client }) => `#/setup/${client}`,
@@ -78,7 +61,7 @@ function safeGetParam(
return ps[n][0];
}
-function safeToURL(s: string | undefined): URL | undefined {
+export function safeToURL(s: string | undefined): URL | undefined {
if (s === undefined) return undefined;
try {
return new URL(s);
@@ -88,71 +71,75 @@ function safeToURL(s: string | undefined): URL | undefined {
}
function PublicRounting(): VNode {
- const location = useCurrentLocation(publicPages);
+ const loc = useCurrentLocation(publicPages);
const { navigateTo } = useNavigationContext();
- const { start } = useSessionState();
+ useErrorBoundary((e) => {
+ console.log("error", e);
+ });
- if (location === undefined) {
- return <NonceNotFound />;
- }
+ const location: typeof loc =
+ loc.name === undefined
+ ? {
+ ...loc,
+ name: "authorize",
+ }
+ : loc;
switch (location.name) {
case "noinfo": {
return <div>no info</div>;
}
case "setup": {
+ const secret = safeGetParam(location.params, "secret");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+
return (
<Setup
clientId={location.values.client}
- onCreated={(nonce) => {
- navigateTo(publicPages.ask.url({ nonce }));
- //response_type=code
- //client_id=1
- //redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet
- //state=123
+ secret={secret}
+ redirectURL={redirectURL}
+ onCreated={() => {
+ navigateTo(publicPages.ask.url({}));
}}
/>
);
}
case "authorize": {
- const responseType = safeGetParam(location.params, "response_type");
const clientId = safeGetParam(location.params, "client_id");
const redirectURL = safeToURL(
safeGetParam(location.params, "redirect_uri"),
);
const state = safeGetParam(location.params, "state");
- // http://localhost:8080/app/#/authorize/ASDASD123?response_type=code&client_id=1&redirect_uri=goog.ecom&state=123
- //
+ const nonce = safeGetParam(location.params, "nonce");
- // http://localhost:8080/app/?response_type=code&client_id=1&redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet&state=123#/authorize/X9668AR2CFC26X55H0M87GJZXGM45VD4SZE05C5SNS5FADPWN220
+ const sessionId: SessionId | undefined =
+ !clientId || !redirectURL || !state || !nonce
+ ? undefined
+ : {
+ clientId,
+ nonce: nonce,
+ redirectURL: redirectURL.href,
+ state,
+ };
- if (
- !responseType ||
- !clientId ||
- !redirectURL ||
- !state ||
- responseType !== "code"
- ) {
- return <MissingParams />;
+ if (!sessionId) {
+ return (
+ <div>
+ one of the params is missing{" "}
+ {JSON.stringify(
+ { clientId, redirectURL, state, nonce },
+ undefined,
+ 2,
+ )}
+ </div>
+ );
}
- const sessionId: SessionId = {
- clientId,
- redirectURL: redirectURL.href,
- state,
- };
return (
<CheckChallengeIsUpToDate
- sessionId={sessionId}
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
+ session={sessionId}
onCompleted={() => {
- start(sessionId);
navigateTo(
publicPages.completed.url({
nonce: location.values.nonce,
@@ -160,7 +147,6 @@ function PublicRounting(): VNode {
);
}}
onChangeLeft={() => {
- start(sessionId);
navigateTo(
publicPages.ask.url({
nonce: location.values.nonce,
@@ -168,7 +154,6 @@ function PublicRounting(): VNode {
);
}}
onNoMoreChanges={() => {
- start(sessionId);
navigateTo(
publicPages.ask.url({
nonce: location.values.nonce,
@@ -176,93 +161,112 @@ function PublicRounting(): VNode {
);
}}
>
- <Loading />
+ No nonce has been found
</CheckChallengeIsUpToDate>
);
}
case "ask": {
+ const clientId = safeGetParam(location.params, "client_id");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+ const state = safeGetParam(location.params, "state");
+ const nonce = safeGetParam(location.params, "nonce");
+
+ const sessionId: SessionId | undefined =
+ !clientId || !redirectURL || !state || !nonce
+ ? undefined
+ : {
+ clientId,
+ nonce: nonce,
+ redirectURL: redirectURL.href,
+ state,
+ };
+
+ if (!sessionId) {
+ return (
+ <div>
+ one of the params is missing{" "}
+ {JSON.stringify(sessionId, undefined, 2)}
+ </div>
+ );
+ }
return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
+ <AskChallenge
+ session={sessionId}
+ focus
+ routeSolveChallenge={publicPages.answer}
+ onSendSuccesful={() => {
navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- onCompleted={() => {
- navigateTo(
- publicPages.completed.url({
+ publicPages.answer.url({
nonce: location.values.nonce,
}),
);
}}
- >
- <AskChallenge
- focus
- nonce={location.values.nonce}
- routeSolveChallenge={publicPages.answer}
- onSendSuccesful={() => {
- navigateTo(
- publicPages.answer.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- />
- </CheckChallengeIsUpToDate>
+ // onCompleted={() => {
+ // navigateTo(
+ // publicPages.completed.url({
+ // nonce: location.values.nonce,
+ // }),
+ // );
+ // }}
+ />
);
}
case "answer": {
+ const clientId = safeGetParam(location.params, "client_id");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+ const state = safeGetParam(location.params, "state");
+ const nonce = safeGetParam(location.params, "nonce");
+
+ const sessionId: SessionId | undefined =
+ !clientId || !redirectURL || !state || !nonce
+ ? undefined
+ : {
+ clientId,
+ nonce: nonce,
+ redirectURL: redirectURL.href,
+ state,
+ };
+
+ if (!sessionId) {
+ return (
+ <div>
+ one of the params is missing{" "}
+ {JSON.stringify(
+ { clientId, redirectURL, state, nonce },
+ undefined,
+ 2,
+ )}
+ </div>
+ );
+ }
return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- onCompleted={() => {
+ <AnswerChallenge
+ focus
+ session={sessionId}
+ routeAsk={publicPages.ask}
+ onComplete={() => {
navigateTo(
publicPages.completed.url({
nonce: location.values.nonce,
}),
);
}}
- >
- <AnswerChallenge
- focus
- nonce={location.values.nonce}
- routeAsk={publicPages.ask}
- onComplete={() => {
- navigateTo(
- publicPages.completed.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- />
- </CheckChallengeIsUpToDate>
+ // onCompleted={() => {
+ // navigateTo(
+ // publicPages.completed.url({
+ // nonce: location.values.nonce,
+ // }),
+ // );
+ // }}
+ />
);
}
case "completed": {
- return (
- <CheckChallengeIsUpToDate
- nonce={location.values.nonce}
- onNoInfo={() => {
- navigateTo(
- publicPages.noinfo.url({
- nonce: location.values.nonce,
- }),
- );
- }}
- >
- <CallengeCompleted nonce={location.values.nonce} />
- </CheckChallengeIsUpToDate>
- );
+ return <CallengeCompleted />;
}
default:
assertUnreachable(location);
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
index 2b5c5c815..655e46a5c 100644
--- a/packages/challenger-ui/src/app.tsx
+++ b/packages/challenger-ui/src/app.tsx
@@ -29,18 +29,16 @@ import {
TalerWalletIntegrationBrowserProvider,
TranslationProvider,
} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { SWRConfig } from "swr";
import { Routing } from "./Routing.js";
-// import { BankCoreApiProvider } from "./context/config.js";
-// import { BrowserHashNavigationProvider } from "./context/navigation.js";
import { SettingsProvider } from "./context/settings.js";
-// import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js";
-import { VNode, h } from "preact";
+import { revalidateChallengeSession } from "./hooks/challenge.js";
import { strings } from "./i18n/strings.js";
-import { ChallengerUiSettings, fetchSettings } from "./settings.js";
import { Frame } from "./pages/Frame.js";
-import { revalidateChallengeSession } from "./hooks/challenge.js";
+import { ChallengerUiSettings, fetchSettings } from "./settings.js";
+
const WITH_LOCAL_STORAGE_CACHE = false;
const evictBankSwrCache: CacheEvictor<ChallengerCacheEviction> = {
@@ -50,6 +48,10 @@ const evictBankSwrCache: CacheEvictor<ChallengerCacheEviction> = {
await Promise.all([revalidateChallengeSession()]);
return;
}
+ case ChallengerCacheEviction.SOLVE_CHALLENGE: {
+ await Promise.all([revalidateChallengeSession()]);
+ return;
+ }
default: {
assertUnreachable(op);
}
diff --git a/packages/challenger-ui/src/attempts-exhausted.html b/packages/challenger-ui/src/attempts-exhausted.html
deleted file mode 100644
index c2468b98b..000000000
--- a/packages/challenger-ui/src/attempts-exhausted.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Attempts exhausted (#{{ec}})</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
- <div class="rounded-md bg-red-50 p-4 shadow-xl">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
- class="w-8 h-8 text-red-400">
- <path fill-rule="evenodd"
- d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">
- You have tried too many times
- </h3>
- <div class="mt-2 text-sm text-red-700">
- <p>More attempts are not allowed</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
index 70e41bf1e..fecb36cbb 100644
--- a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -28,42 +28,23 @@ import { useChallengeSession } from "../hooks/challenge.js";
import { SessionId, useSessionState } from "../hooks/session.js";
interface Props {
- nonce: string;
+ session: SessionId;
children: ComponentChildren;
- sessionId?: SessionId;
onCompleted?: () => void;
onChangeLeft?: () => void;
onNoMoreChanges?: () => void;
- onNoInfo: () => void;
}
export function CheckChallengeIsUpToDate({
- sessionId: sessionFromParam,
- nonce,
+ session,
children,
onCompleted,
onChangeLeft,
onNoMoreChanges,
- onNoInfo,
}: Props): VNode {
- const { state, updateStatus } = useSessionState();
+ const { state } = useSessionState();
const { i18n } = useTranslationContext();
- const sessionId = sessionFromParam
- ? sessionFromParam
- : !state
- ? undefined
- : {
- clientId: state.clientId,
- redirectURL: state.redirectURL,
- state: state.state,
- };
-
- const result = useChallengeSession(nonce, sessionId);
- console.log("asd");
- if (!sessionId) {
- onNoInfo();
- return <Loading />;
- }
+ const result = useChallengeSession(session);
if (!result) {
return <Loading />;
@@ -106,14 +87,31 @@ export function CheckChallengeIsUpToDate({
</Attention>
);
}
+ case HttpStatusCode.TooManyRequests: {
+ return (
+ <Fragment>
+ <Attention
+ type="danger"
+ title={i18n.str`Can't complete this challenge`}
+ >
+ <i18n.Translate>
+ There have been too many attempts to request challenge
+ transmissions and check the TAN code.
+ </i18n.Translate>
+ </Attention>
+
+ <div class="mt-2">
+ <a href={session.redirectURL ?? ""}>{session.redirectURL}</a>
+ </div>
+ </Fragment>
+ );
+ }
default:
assertUnreachable(result);
}
}
- updateStatus(result.body);
-
- if (onCompleted && "redirectURL" in result.body) {
+ if (onCompleted && result.body.solved) {
onCompleted();
return <Loading />;
}
@@ -123,7 +121,7 @@ export function CheckChallengeIsUpToDate({
return <Loading />;
}
- if (onChangeLeft && !result.body.changes_left) {
+ if (onChangeLeft && result.body.changes_left) {
onChangeLeft();
return <Loading />;
}
diff --git a/packages/challenger-ui/src/context/preferences.ts b/packages/challenger-ui/src/context/preferences.ts
new file mode 100644
index 000000000..3188bd71c
--- /dev/null
+++ b/packages/challenger-ui/src/context/preferences.ts
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ showChallangeSetup: boolean;
+ showDebugInfo: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("showChallangeSetup", codecForBoolean())
+ .property("showDebugInfo", codecForBoolean())
+ .build("Preferences");
+
+const defaultPreferences: Preferences = {
+ showChallangeSetup: false,
+ showDebugInfo: false,
+};
+
+const PREFERENCES_KEY = buildStorageKey(
+ "challenger-preferences",
+ codecForPreferences(),
+);
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ PREFERENCES_KEY,
+ defaultPreferences,
+ );
+
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "showChallangeSetup",
+ "showDebugInfo",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "showChallangeSetup":
+ return i18n.str`Show challenger setup screen`;
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ }
+}
diff --git a/packages/challenger-ui/src/declaration.d.ts b/packages/challenger-ui/src/declaration.d.ts
new file mode 100644
index 000000000..581cbcd07
--- /dev/null
+++ b/packages/challenger-ui/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: string;
+ export default content;
+}
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/challenger-ui/src/enter-address-form.html b/packages/challenger-ui/src/enter-address-form.html
deleted file mode 100644
index 76b4d2262..000000000
--- a/packages/challenger-ui/src/enter-address-form.html
+++ /dev/null
@@ -1,133 +0,0 @@
-<!--
- 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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Enter contact details</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="isolate bg-white px-6 py-12">
- <div class="mx-auto max-w-2xl text-center">
- <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
- Enter contact details
- </h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- You will receive a letter with a TAN code that must be provided on the next page.
- </p>
- </div>
- <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
- <div class="sm:col-span-2">
- <label for="address" class="block text-sm font-semibold leading-6 text-gray-900">
- Street address
- </label>
- <div class="mt-2.5">
- <textarea name="address" id="address" rows="3" autocomplete="shipping street-address"
- class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"></textarea>
-
- </div>
- </div>
-
- <div class="sm:col-span-2">
- <label for="city" class="block text-sm font-semibold leading-6 text-gray-900">
- City
- </label>
- <div class="mt-2.5">
- <input type="text" name="city" id="city" maxlength="512" autocomplete="address-level2"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <div class="sm:col-span-2">
- <label for="postal-code" class="block text-sm font-semibold leading-6 text-gray-900">
- Postal code
- </label>
- <div class="mt-2.5">
- <input type="text" name="postal-code" id="postal-code" maxlength="512" autocomplete="postal-code"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <div class="sm:col-span-2">
- <label for="country" class="block text-sm font-semibold leading-6 text-gray-900">
- Country
- </label>
- <div class="mt-2.5">
- <input type="text" name="country" id="country" maxlength="512" autocomplete="country"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- You can change address another {{changes_left}} times.
- </p>
- </div>
-
- <div class="mt-10">
- <button type="submit"
- class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
- Send mail
- </button>
- </div>
- </form>
- </div>
- </main>
-
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html>
diff --git a/packages/challenger-ui/src/enter-email-form.html b/packages/challenger-ui/src/enter-email-form.html
deleted file mode 100644
index 3b8720244..000000000
--- a/packages/challenger-ui/src/enter-email-form.html
+++ /dev/null
@@ -1,127 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Enter contact details</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="isolate bg-white px-6 py-12">
- <div class="mx-auto max-w-2xl text-center">
- <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
- Enter contact details
- </h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- You will receive an email with a TAN code that must be provided on the next page.
- </p>
- </div>
- <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
- <div class="sm:col-span-2">
- <label for="email" class="block text-sm font-semibold leading-6 text-gray-900">
- Email
- </label>
- <div class="mt-2.5">
- <input type="email" name="email" id="email" maxlength="512" autocomplete="email" value="{{last_address}}"
- {{#fixed_address}}readonly{{/fixed_address}}
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <script>
- function check() {
- var email = document.getElementById('email');
- var emailRepeat = document.getElementById('repeat-email');
-
- if (email.value != emailRepeat.value) {
- emailRepeat.setCustomValidity('The two email addresses must match.');
- } else {
- // input is valid -- reset the error message
- emailRepeat.setCustomValidity('');
- }
- }
- </script>
-
- <div class="sm:col-span-2">
- <label for="repeat-email" class="block text-sm font-semibold leading-6 text-gray-900">
- Repeat email
- </label>
- <div class="mt-2.5">
- <input oninput="check(this)" type="email" name="repeat-email" id="repeat-email" autocomplete="email"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- You can change your email address another {{changes_left}} times.
- </p>
- </div>
-
- <div class="mt-10">
- <button type="submit"
- class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
- Send email
- </button>
- </div>
- </form>
- </div>
- </main>
-
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html>
diff --git a/packages/challenger-ui/src/enter-file-access-form.html b/packages/challenger-ui/src/enter-file-access-form.html
deleted file mode 100644
index b79d1dada..000000000
--- a/packages/challenger-ui/src/enter-file-access-form.html
+++ /dev/null
@@ -1,102 +0,0 @@
-<!--
- 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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Enter local file name</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="isolate bg-white px-6 py-12">
- <div class="mx-auto max-w-2xl text-center">
- <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
- Enter file name
- </h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- The file will be overwritten with a code which need to be entered in the next page.
- </p>
- </div>
- <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
- <div class="sm:col-span-2">
- <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
- Phone number
- </label>
- <div class="mt-2.5">
- <input type="filename" name="filename" id="filename" maxlength="200"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- You can change the filename another {{changes_left}} times.
- </p>
- </div>
-
- <div class="mt-10">
- <button type="submit"
- class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
- Write file
- </button>
- </div>
- </form>
- </div>
- </main>
-
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html>
diff --git a/packages/challenger-ui/src/enter-phone-form.html b/packages/challenger-ui/src/enter-phone-form.html
deleted file mode 100644
index ca06fb94e..000000000
--- a/packages/challenger-ui/src/enter-phone-form.html
+++ /dev/null
@@ -1,126 +0,0 @@
-<!--
- 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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Enter contact details</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="isolate bg-white px-6 py-12">
- <div class="mx-auto max-w-2xl text-center">
- <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
- Enter contact details
- </h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- You will receive an SMS with a TAN code that must be provided on the next page.
- </p>
- </div>
- <form action="/challenge/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
- <div class="sm:col-span-2">
- <label for="phone" class="block text-sm font-semibold leading-6 text-gray-900">
- Phone number
- </label>
- <div class="mt-2.5">
- <input type="phone" name="phone" id="phone" maxlength="20" autocomplete="tel"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <script>
- function check() {
- var phone = document.getElementById('phone');
- var phoneRepeat = document.getElementById('repeat-phone');
-
- if (phone.value != phoneRepeat.value) {
- phoneRepeat.setCustomValidity('The two phone numbers must match.');
- } else {
- // input is valid -- reset the error message
- phoneRepeat.setCustomValidity('');
- }
- }
- </script>
-
- <div class="sm:col-span-2">
- <label for="repeat-phone" class="block text-sm font-semibold leading-6 text-gray-900">
- Repeat phone
- </label>
- <div class="mt-2.5">
- <input oninput="check(this)" type="number" name="repeat-phone" id="repeat-phone" autocomplete="tel"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
- </div>
- </div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- You can change your phone number another {{changes_left}} times.
- </p>
- </div>
-
- <div class="mt-10">
- <button type="submit"
- class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
- Send SMS
- </button>
- </div>
- </form>
- </div>
- </main>
-
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html>
diff --git a/packages/challenger-ui/src/enter-tan-form.html b/packages/challenger-ui/src/enter-tan-form.html
deleted file mode 100644
index 965f8e9d2..000000000
--- a/packages/challenger-ui/src/enter-tan-form.html
+++ /dev/null
@@ -1,117 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Enter your TAN</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="isolate bg-white px-6 py-12">
- <div class="mx-auto max-w-2xl text-center">
- <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
- Please enter the TAN you received to authenticate.
- </h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- <!-- {{#transmitted}}
- A TAN was sent to your address &quot;{{address}}&quot;.
- {{/transmitted}} -->
- <!-- {{^transmitted}} -->
- We recently already sent a TAN to your address &quot;{{address}}&quot;.
- A new TAN will not be transmitted again before {{next_tx_time}}.
- <!-- {{/transmitted}} -->
- </p>
- </div>
-
-
- <form action="/solve/{{nonce}}" method="POST" class="mx-auto mt-16 max-w-xl sm:mt-20">
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
- <div class="sm:col-span-2">
- <label for="pin" class="block text-sm font-semibold leading-6 text-gray-900">
- TAN code
- </label>
- <div class="mt-2.5">
- <div
- class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600">
- <span class="flex select-none items-center pl-3 text-gray-500 sm:text-sm">TAN:</span>
- <input type="number" name="pin" id="pin" maxlength="64"
- class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
- placeholder="12345678">
- </div>
-
- </div>
- </div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- You have {{attempts_left}} attempts left.
- </p>
- </div>
-
- <div class="mt-10">
- <button type="submit"
- class="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
- Check
- </button>
- </div>
- </form>
-
- </div>
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-
-</body>
-
-</html>
diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
index 846242816..81cceec3f 100644
--- a/packages/challenger-ui/src/hooks/challenge.ts
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -30,27 +30,24 @@ export function revalidateChallengeSession() {
);
}
-export function useChallengeSession(
- nonce: string,
- session: SessionId | undefined,
-) {
+export function useChallengeSession(session: SessionId) {
const {
lib: { challenger: api },
} = useChallengerApiContext();
- async function fetcher([n, c, r, s]: [string, string, string, string]) {
- return await api.login(n, c, r, s);
+ async function fetcher([s]: [SessionId]) {
+ return await api.login(s.nonce, s.clientId, s.redirectURL, s.state);
}
const { data, error } = useSWR<
ChallengerResultByMethod<"login">,
TalerHttpError
- >(
- !session
- ? undefined
- : [nonce, session.clientId, session.redirectURL, session.state, "login"],
- fetcher,
- {},
- );
+ >(!session ? undefined : [session, "login"], fetcher, {
+ revalidateIfStale: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
if (data) return data;
if (error) return error;
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
index ed7ea8986..86dfff94e 100644
--- a/packages/challenger-ui/src/hooks/session.ts
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -15,63 +15,69 @@
*/
import {
+ AbsoluteTime,
ChallengerApi,
Codec,
buildCodecForObject,
- codecForBoolean,
- codecForChallengeStatus,
- codecForNumber,
+ codecForAbsoluteTime,
+ codecForAny,
+ codecForList,
codecForString,
codecForStringURL,
codecOptional,
} from "@gnu-taler/taler-util";
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
-import { mutate } from "swr";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
export type SessionId = {
+ nonce: string;
clientId: string;
redirectURL: string;
state: string;
};
export type LastChallengeResponse = {
- attemptsLeft: number;
- nextSend: string;
+ sendCodeLeft: number;
+ changeTargetLeft: number;
+ checkPinLeft: number;
+ nextSend: AbsoluteTime;
transmitted: boolean;
};
-export type SessionState = SessionId & {
- lastTry: LastChallengeResponse | undefined;
- lastStatus: ChallengerApi.ChallengeStatus | undefined;
+interface LastAddress {
+ address: Record<string, string>;
+ type: string;
+ savedAt: AbsoluteTime;
+}
+
+export type SessionState = {
completedURL: string | undefined;
+ lastAddress: Array<LastAddress> | undefined;
};
-export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
- buildCodecForObject<LastChallengeResponse>()
- .property("attemptsLeft", codecForNumber())
- .property("nextSend", codecForString())
- .property("transmitted", codecForBoolean())
- .build("LastChallengeResponse");
+export const codecForLastAddress = (): Codec<LastAddress> =>
+ buildCodecForObject<LastAddress>()
+ .property("address", codecForAny())
+ .property("type", codecForString())
+ .property("savedAt", codecForAbsoluteTime)
+ .build("LastAddress");
export const codecForSessionState = (): Codec<SessionState> =>
buildCodecForObject<SessionState>()
- .property("clientId", codecForString())
- .property("redirectURL", codecForStringURL())
.property("completedURL", codecOptional(codecForStringURL()))
- .property("state", codecForString())
- .property("lastStatus", codecOptional(codecForChallengeStatus()))
- .property("lastTry", codecOptional(codecForLastChallengeResponse()))
+ .property("lastAddress", codecOptional(codecForList(codecForLastAddress())))
.build("SessionState");
export interface SessionStateHandler {
state: SessionState | undefined;
start(s: SessionId): void;
- accepted(l: LastChallengeResponse): void;
- completed(e: URL): void;
- updateStatus(s: ChallengerApi.ChallengeStatus): void;
+ saveAddress(type: string, address: Record<string, string>): void;
+ removeAddress(index: number): void;
+ sent(info: ChallengerApi.ChallengeCreateResponse): void;
+ failed(info: ChallengerApi.InvalidPinResponse): void;
+ completed(info: ChallengerApi.ChallengeRedirect): void;
}
const SESSION_STATE_KEY = buildStorageKey(
@@ -89,55 +95,48 @@ export function useSessionState(): SessionStateHandler {
return {
state,
- start(info) {
+ start() {
update({
- ...info,
- lastTry: undefined,
completedURL: undefined,
- lastStatus: undefined,
+ lastAddress: state?.lastAddress ?? [],
});
- cleanAllCache();
},
- accepted(lastTry) {
- if (!state) return;
+ removeAddress(index) {
+ const lastAddr = [...(state?.lastAddress ?? [])];
+ lastAddr.splice(index, 1);
update({
- ...state,
- lastTry,
+ completedURL: undefined,
+ lastAddress: lastAddr,
});
},
- completed(url) {
- if (!state) return;
+ saveAddress(type, address) {
+ const lastAddr = [...(state?.lastAddress ?? [])];
+ lastAddr.push({
+ type,
+ address: address ?? {},
+ savedAt: AbsoluteTime.now(),
+ });
update({
- ...state,
- completedURL: url.href,
+ completedURL: undefined,
+ lastAddress: lastAddr,
});
},
- updateStatus(st: ChallengerApi.ChallengeStatus) {
- if (!state) return;
- if (!state.lastStatus) {
+ sent(info) {
+ },
+ failed(info) {
+ },
+ completed(info) {
+ if (!state) {
update({
- ...state,
- lastStatus: st,
+ completedURL: info.redirect_url,
+ lastAddress: [],
});
- return;
- }
- // current status
- const ls = state.lastStatus;
- if (
- ls.changes_left !== st.changes_left ||
- ls.fix_address !== st.fix_address ||
- ls.last_address !== st.last_address
- ) {
+ } else {
update({
- ...state,
- lastStatus: st,
+ completedURL: info.redirect_url,
+ lastAddress: state.lastAddress,
});
- return;
}
},
};
}
-
-function cleanAllCache(): void {
- mutate(() => true, undefined, { revalidate: false });
-}
diff --git a/packages/challenger-ui/src/i18n/challenger-ui.pot b/packages/challenger-ui/src/i18n/challenger-ui.pot
index 5d2497acf..c1501192f 100644
--- a/packages/challenger-ui/src/i18n/challenger-ui.pot
+++ b/packages/challenger-ui/src/i18n/challenger-ui.pot
@@ -15,7 +15,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: Taler Bank\n"
+"Project-Id-Version: Challenger\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"
diff --git a/packages/challenger-ui/src/internal-error.html b/packages/challenger-ui/src/internal-error.html
deleted file mode 100644
index 521a2b69e..000000000
--- a/packages/challenger-ui/src/internal-error.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Internal server error (#{{ec}})</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
- <div class="rounded-md bg-red-50 p-4 shadow-xl">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
- class="w-8 h-8 text-red-400">
- <path fill-rule="evenodd"
- d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">
- Internal error
- </h3>
- <div class="mt-2 text-sm text-red-700">
- <p>{{hint}} ({{detail}})</p>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-pin.html b/packages/challenger-ui/src/invalid-pin.html
deleted file mode 100644
index 1229b8095..000000000
--- a/packages/challenger-ui/src/invalid-pin.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Invalid solution (#{{ec}})</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
- <div class="rounded-md bg-red-50 p-4 shadow-xl">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
- class="w-8 h-8 text-red-400">
- <path fill-rule="evenodd"
- d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">
- Invalid PIN
- </h3>
- <div class="mt-2 text-sm text-red-700">
- <p>{{hint}}</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/invalid-request.html b/packages/challenger-ui/src/invalid-request.html
deleted file mode 100644
index 89e6b125c..000000000
--- a/packages/challenger-ui/src/invalid-request.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Invalid request (#{{ec}})</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
- <div class="rounded-md bg-red-50 p-4 shadow-xl">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
- class="w-8 h-8 text-red-400">
- <path fill-rule="evenodd"
- d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">
- Request error
- </h3>
- <div class="mt-2 text-sm text-red-700">
- <p>{{hint}}</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index 73a79c51f..48f4db477 100644
--- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -14,8 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- ChallengerApi,
+ AbsoluteTime,
+ EmptyObject,
HttpStatusCode,
+ TalerError,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
@@ -24,118 +26,268 @@ import {
LocalNotificationBanner,
RouteDefinition,
ShowInputErrorLabel,
+ Time,
useChallengerApiContext,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { useSessionState } from "../hooks/session.js";
-
-export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+import { useEffect, useState } from "preact/hooks";
+import {
+ revalidateChallengeSession,
+ useChallengeSession,
+} from "../hooks/challenge.js";
+import { SessionId, useSessionState } from "../hooks/session.js";
type Props = {
- nonce: string;
focus?: boolean;
+ session: SessionId,
onComplete: () => void;
- routeAsk: RouteDefinition<{ nonce: string }>;
+ routeAsk: RouteDefinition<EmptyObject>;
};
-export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props): VNode {
- const { lib } = useChallengerApiContext();
+function useReloadOnDeadline(deadline: AbsoluteTime): void {
+ const [, set] = useState(false);
+ function toggle(): void {
+ set((s) => !s);
+ }
+ useEffect(() => {
+ if (AbsoluteTime.isExpired(deadline)) {
+ return;
+ }
+ const diff = AbsoluteTime.difference(AbsoluteTime.now(), deadline);
+ if (diff.d_ms === "forever") return;
+ const timer = setTimeout(toggle, diff.d_ms);
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [deadline]);
+}
+
+export function AnswerChallenge({ session, focus, onComplete, routeAsk }: Props): VNode {
+ const { config, lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
- const { state, accepted, completed } = useSessionState();
+ const { sent, failed, completed } = useSessionState();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [pin, setPin] = useState<string | undefined>();
- const [lastTryError, setLastTryError] =
- useState<ChallengerApi.InvalidPinResponse>();
const errors = undefinedIfEmpty({
pin: !pin ? i18n.str`Can't be empty` : undefined,
});
- const lastEmail = !state
+ const restrictionKeys = !config.restrictions
+ ? []
+ : Object.keys(config.restrictions);
+ const restrictionKey = !restrictionKeys.length
? undefined
- : !state.lastStatus
+ : restrictionKeys[0];
+
+ const result = useChallengeSession(session);
+
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ const deadline =
+ lastStatus == undefined
? undefined
- : !state.lastStatus.last_address
- ? undefined
- : state.lastStatus.last_address["email"];
+ : AbsoluteTime.fromProtocolTimestamp(lastStatus.retransmission_time);
+
+ useReloadOnDeadline(deadline ?? AbsoluteTime.never());
+
+ if (!restrictionKey) {
+ return (
+ <div>
+ invalid server configuration, there is no restriction in /config
+ </div>
+ );
+ }
+
+ const lastAddr = !lastStatus?.last_address
+ ? undefined
+ : lastStatus.last_address[restrictionKey];
+
+ const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1;
+ const contact = lastAddr ? { [restrictionKey]: lastAddr } : undefined;
const onSendAgain =
- !state || lastEmail === undefined
+ contact === undefined ||
+ lastStatus == undefined ||
+ lastStatus.pin_transmissions_left === 0 ||
+ !deadline ||
+ !AbsoluteTime.isExpired(deadline)
? undefined
: withErrorHandler(
async () => {
- if (!lastEmail) return;
- return await lib.challenger.challenge(nonce, { email: lastEmail });
+ return await lib.challenger.challenge(session.nonce, contact);
},
(ok) => {
- if ("redirectURL" in ok.body) {
- completed(ok.body.redirectURL);
+ if (ok.body.type === "completed") {
+ completed(ok.body);
} else {
- accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
- transmitted: ok.body.transmitted,
- });
+ sent(ok.body);
}
- return undefined;
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str``;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
}
},
);
const onCheck =
- errors !== undefined || (lastTryError && lastTryError.exhausted)
+ errors !== undefined ||
+ lastStatus == undefined ||
+ lastStatus.auth_attempts_left === 0
? undefined
: withErrorHandler(
async () => {
- return lib.challenger.solve(nonce, { pin: pin! });
+ return lib.challenger.solve(session.nonce, { pin: pin! });
},
(ok) => {
- completed(ok.body.redirectURL as URL);
+ if (ok.body.type === "completed") {
+ completed(ok.body);
+ } else {
+ failed(ok.body);
+ }
onComplete();
},
(fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
- return i18n.str`Invalid request`;
+ return i18n.str`The request was not accepted, try reloading the app.`;
case HttpStatusCode.Forbidden: {
- setLastTryError(fail.body);
- return i18n.str`Invalid pin`;
+ revalidateChallengeSession();
+ return i18n.str`Invalid pin.`;
}
case HttpStatusCode.NotFound:
- return i18n.str``;
+ return i18n.str`Challenge not found.`;
case HttpStatusCode.NotAcceptable:
- return i18n.str``;
- case HttpStatusCode.TooManyRequests:
- return i18n.str``;
+ return i18n.str`Server templates are missing due to misconfiguration.`;
+ case HttpStatusCode.TooManyRequests: {
+ revalidateChallengeSession();
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
+ }
case HttpStatusCode.InternalServerError:
- return i18n.str``;
+ return i18n.str`Server is not able to respond due to internal problems.`;
default:
assertUnreachable(fail);
}
},
);
+ const cantTryAnymore = lastStatus?.auth_attempts_left === 0;
+
+ function LastContactSent(): VNode {
+ return (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {!lastStatus || !deadline || AbsoluteTime.isExpired(deadline) ? (
+ <i18n.Translate>
+ Last TAN code was sent to your address &quot;{lastAddr}
+ &quot; is not valid anymore.
+ </i18n.Translate>
+ ) : (
+ <Attention
+ title={i18n.str`A TAN code was sent to your address "${lastAddr}"`}
+ >
+ <i18n.Translate>
+ You should wait until &quot;
+ <Time format="dd/MM/yyyy HH:mm:ss" timestamp={deadline} />
+ &quot; to send a new one.
+ </i18n.Translate>
+ </Attention>
+ )}
+ </p>
+ );
+ }
- if (!state) {
- return <div>no state</div>;
+ function TryAnotherCode(): VNode {
+ return (
+ <div class="mx-auto mt-4 max-w-xl flex justify-between">
+ <div>
+ <a
+ data-disabled={unableToChangeAddr}
+ href={unableToChangeAddr ? undefined : routeAsk.url({})}
+ class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ >
+ <i18n.Translate>Try with another address</i18n.Translate>
+ </a>
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the contact address anymore.
+ </i18n.Translate>
+ ) : lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the contact address one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the contact address {lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
+ </p>
+ )}
+ </div>
+ <div>
+ <Button
+ type="submit"
+ disabled={!onSendAgain}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSendAgain}
+ >
+ <i18n.Translate>Send new code</i18n.Translate>
+ </Button>
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {lastStatus.pin_transmissions_left < 1 ? (
+ <i18n.Translate>
+ We can&#39;t send you the code anymore.
+ </i18n.Translate>
+ ) : lastStatus.pin_transmissions_left === 1 ? (
+ <i18n.Translate>
+ We can send the code one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ We can send the code {lastStatus.pin_transmissions_left} more
+ times.
+ </i18n.Translate>
+ )}
+ </p>
+ )}
+ </div>
+ </div>
+ );
}
- if (!state.lastTry) {
- return <div>you should do a challenge first</div>;
+ if (cantTryAnymore) {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>Last TAN code can not be used.</i18n.Translate>
+ </h2>
+
+ <LastContactSent />
+ </div>
+
+ <TryAnotherCode />
+ </div>
+ </Fragment>
+ );
}
return (
@@ -149,33 +301,31 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
Enter the TAN you received to authenticate.
</i18n.Translate>
</h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- {state.lastTry.transmitted ? (
- <i18n.Translate>
- A TAN was sent to your address &quot;{lastEmail}&quot;.
- </i18n.Translate>
- ) : (
- <Attention title={i18n.str`Resend failed`} type="warning">
+ <LastContactSent />
+
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {lastStatus.auth_attempts_left < 1 ? (
<i18n.Translate>
- We recently already sent a TAN to your address &quot;
- {lastEmail}&quot;. A new TAN will not be transmitted again
- before &quot;{state.lastTry.nextSend}&quot;.
+ You can&#39;t check the PIN anymore.
</i18n.Translate>
- </Attention>
- )}
- </p>
- {!lastTryError ? undefined : (
- <p class="mt-2 text-lg leading-8 text-gray-600">
- <i18n.Translate>
- You can try another PIN but just{" "}
- {lastTryError.auth_attempts_left} times more.
- </i18n.Translate>
+ ) : lastStatus.auth_attempts_left === 1 ? (
+ <i18n.Translate>
+ You can check the PIN one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can check the PIN {lastStatus.auth_attempts_left} more
+ times.
+ </i18n.Translate>
+ )}
</p>
)}
</div>
+
<form
method="POST"
- class="mx-auto mt-16 max-w-xl sm:mt-20"
+ class="mx-auto mt-4 max-w-xl"
onSubmit={(e) => {
e.preventDefault();
}}
@@ -209,12 +359,6 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
/>
</div>
</div>
-
- <p class="mt-3 text-sm leading-6 text-gray-400">
- <i18n.Translate>
- You have {state.lastTry.attemptsLeft} attempts left.
- </i18n.Translate>
- </p>
</div>
<div class="mt-10">
@@ -227,27 +371,9 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
<i18n.Translate>Check</i18n.Translate>
</Button>
</div>
- <div class="mt-10 flex justify-between">
- <div>
- <a
- href={routeAsk.url({ nonce })}
- class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- >
- <i18n.Translate>Change email</i18n.Translate>
- </a>
- </div>
- <div>
- <Button
- type="submit"
- disabled={!onSendAgain}
- class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- handler={onSendAgain}
- >
- <i18n.Translate>Send code again</i18n.Translate>
- </Button>
- </div>
- </div>
</form>
+
+ <TryAnotherCode />
</div>
</Fragment>
);
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
index 30b50d707..f034a773b 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -13,219 +13,418 @@
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 {
+ EmptyObject,
+ HttpStatusCode,
+ TalerError,
+ TranslatedString
+} from "@gnu-taler/taler-util";
import {
Attention,
Button,
LocalNotificationBanner,
RouteDefinition,
ShowInputErrorLabel,
+ Time,
useChallengerApiContext,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useSessionState } from "../hooks/session.js";
+import { useChallengeSession } from "../hooks/challenge.js";
+import { SessionId, useSessionState } from "../hooks/session.js";
import { doAutoFocus } from "./AnswerChallenge.js";
-type Form = {
- email: string;
-};
export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
type Props = {
- nonce: string;
onSendSuccesful: () => void;
- routeSolveChallenge: RouteDefinition<{ nonce: string }>;
+ session: SessionId;
+ routeSolveChallenge: RouteDefinition<EmptyObject>;
focus?: boolean;
};
export function AskChallenge({
- nonce,
onSendSuccesful,
routeSolveChallenge,
+ session,
focus,
}: Props): VNode {
- const { state, accepted, completed } = useSessionState();
- const status = state?.lastStatus;
- const prevEmail =
- !status || !status.last_address ? undefined : status.last_address["email"];
- const regexEmail =
- !status || !status.restrictions ? undefined : status.restrictions["email"];
+ const { state, sent, saveAddress, completed } = useSessionState();
+ const { lib, config } = useChallengerApiContext();
- const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const [email, setEmail] = useState<string | undefined>();
+ const [address, setEmail] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>();
+ const [remember, setRemember] = useState<boolean>(false);
+ const [addrIndex, setAddrIndex] = useState<number | undefined>();
+
+ const restrictionKeys = !config.restrictions
+ ? []
+ : Object.keys(config.restrictions);
+ const restrictionKey = !restrictionKeys.length
+ ? undefined
+ : restrictionKeys[0];
+ const result = useChallengeSession(session);
+
+ if (!restrictionKey) {
+ return (
+ <div>
+ invalid server configuration, there is no restriction in /config
+ </div>
+ );
+ }
+
+ const regexEmail = !config.restrictions
+ ? undefined
+ : config.restrictions[restrictionKey];
const regexTest =
regexEmail && regexEmail.regex ? new RegExp(regexEmail.regex) : EMAIL_REGEX;
const regexHint =
regexEmail && regexEmail.hint ? regexEmail.hint : i18n.str`invalid email`;
+ const lastStatus =
+ result && !(result instanceof TalerError) && result.type !== "fail"
+ ? result.body
+ : undefined;
+
+ const prevAddr = !lastStatus?.last_address
+ ? undefined
+ : lastStatus.last_address[restrictionKey];
+
const errors = undefinedIfEmpty({
- email: !email
+ address: !address
? i18n.str`required`
- : !regexTest.test(email)
+ : !regexTest.test(address)
? regexHint
- : prevEmail !== undefined && email === prevEmail
- ? i18n.str`email should be different`
+ : prevAddr !== undefined && address === prevAddr
+ ? i18n.str`can't use the same address`
: undefined,
repeat: !repeat
? i18n.str`required`
- : email !== repeat
- ? i18n.str`emails doesn't match`
+ : address !== repeat
+ ? i18n.str`doesn't match`
: undefined,
});
- const onSend = errors
- ? undefined
- : withErrorHandler(
- async () => {
- return lib.challenger.challenge(nonce, { email: email! });
- },
- (ok) => {
- if ("redirectURL" in ok.body) {
- completed(ok.body.redirectURL);
- } else {
- accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
- transmitted: ok.body.transmitted,
- });
- }
- onSendSuccesful();
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.BadRequest:
- return i18n.str``;
- case HttpStatusCode.NotFound:
- return i18n.str``;
- case HttpStatusCode.NotAcceptable:
- return i18n.str``;
- case HttpStatusCode.TooManyRequests:
- return i18n.str``;
- case HttpStatusCode.InternalServerError:
- return i18n.str``;
- }
- },
- );
+ const contact = address ? { [restrictionKey]: address } : undefined;
- if (!status) {
+ const usableAddrs =
+ !state?.lastAddress || !state.lastAddress.length
+ ? []
+ : state.lastAddress.filter((d) => !!d.address[restrictionKey]);
+
+ const onSend =
+ errors || !contact
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.challenge(session.nonce, contact);
+ },
+ (ok) => {
+ if (ok.body.type === "completed") {
+ completed(ok.body);
+ } else {
+ if (remember) {
+ saveAddress(config.address_type, contact);
+ }
+ sent(ok.body);
+ }
+ onSendSuccesful();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`The request was not accepted, try reloading the app.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Challenge not found.`;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str`Server templates are missing due to misconfiguration.`;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str`There have been too many attempts to request challenge transmissions.`;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str`Server is not able to respond due to internal problems.`;
+ }
+ },
+ );
+
+ if (!lastStatus) {
return <div>no status loaded</div>;
}
return (
<Fragment>
- <LocalNotificationBanner notification={notification} />
+ <LocalNotificationBanner notification={notification} showDebug={true} />
<div class="isolate bg-white px-6 py-12">
<div class="mx-auto max-w-2xl text-center">
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<i18n.Translate>Enter contact details</i18n.Translate>
</h2>
- <p class="mt-2 text-lg leading-8 text-gray-600">
- <i18n.Translate>
- You will receive an email with a TAN code that must be provided on
- the next page.
- </i18n.Translate>
- </p>
+ {config.address_type === "email" ? (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You will receive an email with a TAN code that must be provided
+ on the next page.
+ </i18n.Translate>
+ </p>
+ ) : config.address_type === "phone" ? (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You will receive an SMS with a TAN code that must be provided on
+ the next page.
+ </i18n.Translate>
+ </p>
+ ) : (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You will receive an message with a TAN code that must be
+ provided on the next page.
+ </i18n.Translate>
+ </p>
+ )}
</div>
- {state.lastTry && (
+
+ {lastStatus?.last_address && (
<Fragment>
- <Attention title={i18n.str`A code has been sent to ${prevEmail}`}>
+ <Attention title={i18n.str`A code has been sent to ${prevAddr}`}>
<i18n.Translate>
- <a href={routeSolveChallenge.url({ nonce })} class="underline">
+ <a href={routeSolveChallenge.url({})} class="underline">
<i18n.Translate>Complete the challenge here.</i18n.Translate>
</a>
</i18n.Translate>
</Attention>
</Fragment>
)}
+
+ {!usableAddrs.length ? undefined : (
+ <div class="mx-auto max-w-xl mt-4">
+ <h3>
+ <i18n.Translate>Previous address</i18n.Translate>
+ </h3>
+ <fieldset>
+ <div class="relative -space-y-px rounded-md bg-white">
+ {usableAddrs.map((addr, idx) => {
+ return (
+ <label
+ data-checked={addrIndex === idx}
+ key={idx}
+ class="relative flex border-gray-200 data-[checked=true]:z-10 data-[checked=true]:bg-indigo-50 cursor-pointer flex-col rounded-tl-md rounded-tr-md border p-4 focus:outline-none md:grid md:grid-cols-2 md:pl-4 md:pr-6"
+ >
+ <span class="flex items-center text-sm">
+ <input
+ type="radio"
+ name={`addr-${idx}`}
+ value={addr.address[restrictionKey]}
+ checked={addrIndex === idx}
+ onClick={() => {
+ setAddrIndex(idx);
+ setEmail(addr.address[restrictionKey]);
+ setRepeat(addr.address[restrictionKey]);
+ }}
+ class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 active:ring-2 active:ring-indigo-600 active:ring-offset-2"
+ />
+ <span
+ data-checked={addrIndex === idx}
+ class="ml-3 font-medium text-gray-900 data-[checked=true]:text-indigo-900 "
+ >
+ {addr.address[restrictionKey]}
+ </span>
+ </span>
+ <span
+ data-checked={addrIndex === idx}
+ class="ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-right text-gray-500 data-[checked=true]:text-indigo-700"
+ >
+ <i18n.Translate>
+ Last used at{" "}
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={addr.savedAt}
+ />
+ </i18n.Translate>
+ </span>
+ </label>
+ );
+ })}
+ <label
+ data-checked={addrIndex === undefined}
+ class="relative rounded-bl-md rounded-br-md flex border-gray-200 data-[checked=true]:z-10 data-[checked=true]:bg-indigo-50 cursor-pointer flex-col rounded-tl-md rounded-tr-md border p-4 focus:outline-none md:grid md:grid-cols-2 md:pl-4 md:pr-6"
+ >
+ <span class="flex items-center text-sm">
+ <input
+ type="radio"
+ name="new-addr"
+ value="new-addr"
+ checked={addrIndex === undefined}
+ onClick={() => {
+ setAddrIndex(undefined);
+ setEmail(undefined);
+ setRepeat(undefined);
+ }}
+ class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 active:ring-2 active:ring-indigo-600 active:ring-offset-2"
+ />
+ <span
+ data-checked={addrIndex === undefined}
+ class="ml-3 font-medium text-gray-900 data-[checked=true]:text-indigo-900 "
+ >
+ <i18n.Translate>Use new address</i18n.Translate>
+ </span>
+ </span>
+ </label>
+ </div>
+ </fieldset>
+ </div>
+ )}
+
<form
method="POST"
- class="mx-auto mt-16 max-w-xl sm:mt-20"
+ class="mx-auto mt-4 max-w-xl "
onSubmit={(e) => {
e.preventDefault();
}}
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label
+ for="address"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ {(function (): TranslatedString {
+ switch (config.address_type) {
+ case "email":
+ return i18n.str`Email`;
+ case "phone":
+ return i18n.str`Phone`;
+ }
+ })()}
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="text"
+ name="address"
+ id="address"
+ ref={focus ? doAutoFocus : undefined}
+ maxLength={512}
+ autocomplete={(function (): string {
+ switch (config.address_type) {
+ case "email":
+ return "email";
+ case "phone":
+ return "phone";
+ }
+ })()}
+ value={address ?? ""}
+ onChange={(e) => {
+ setEmail(e.currentTarget.value);
+ }}
+ placeholder={prevAddr}
+ readOnly={lastStatus.fix_address || addrIndex !== undefined}
+ class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.address}
+ isDirty={address !== undefined}
+ />
+ </div>
+ </div>
+
+ {lastStatus.fix_address || addrIndex !== undefined ? undefined : (
<div class="sm:col-span-2">
<label
- for="email"
+ for="repeat-address"
class="block text-sm font-semibold leading-6 text-gray-900"
>
- <i18n.Translate>Email</i18n.Translate>
+ {(function (): TranslatedString {
+ switch (config.address_type) {
+ case "email":
+ return i18n.str`Repeat email`;
+ case "phone":
+ return i18n.str`Repeat phone`;
+ }
+ })()}
</label>
<div class="mt-2.5">
<input
- type="email"
- name="email"
- id="email"
- ref={focus ? doAutoFocus : undefined}
- maxLength={512}
- autocomplete="email"
- value={email}
+ type="text"
+ name="repeat-address"
+ id="repeat-address"
+ value={repeat ?? ""}
onChange={(e) => {
- setEmail(e.currentTarget.value);
+ setRepeat(e.currentTarget.value);
}}
- placeholder={prevEmail}
- readOnly={status.fix_address}
- class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ autocomplete={(function (): string {
+ switch (config.address_type) {
+ case "email":
+ return "email";
+ case "phone":
+ return "phone";
+ }
+ })()}
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
<ShowInputErrorLabel
- message={errors?.email}
- isDirty={email !== undefined}
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
/>
</div>
</div>
+ )}
- {status.fix_address ? undefined : (
- <div class="sm:col-span-2">
- <label
- for="repeat-email"
- class="block text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Repeat email</i18n.Translate>
- </label>
- <div class="mt-2.5">
- <input
- type="email"
- name="repeat-email"
- id="repeat-email"
- value={repeat}
- onChange={(e) => {
- setRepeat(e.currentTarget.value);
- }}
- autocomplete="email"
- class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- />
- <ShowInputErrorLabel
- message={errors?.repeat}
- isDirty={repeat !== undefined}
- />
- </div>
- </div>
- )}
+ {lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the contact address anymore.
+ </i18n.Translate>
+ ) : lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the contact address one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the contact address {lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
+ </p>
+ )}
- {!status.changes_left ? (
- <p class="mt-3 text-sm leading-6 text-gray-400">
- <i18n.Translate>No more changes left</i18n.Translate>
- </p>
- ) : (
- <p class="mt-3 text-sm leading-6 text-gray-400">
+ <div class="flex items-center justify-between py-2">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>
- You can change your email address another{" "}
- {status.changes_left} times.
+ Remember this address for future challenges.
</i18n.Translate>
- </p>
- )}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`remember switch`}
+ data-enabled={remember}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setRemember(!remember);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={remember}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
</div>
-
- {!prevEmail ? (
+ </form>
+ <div class="mx-auto mt-4 max-w-xl ">
+ {!prevAddr ? (
<div class="mt-10">
<Button
type="submit"
@@ -233,7 +432,14 @@ export function AskChallenge({
class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
handler={onSend}
>
- <i18n.Translate>Send email</i18n.Translate>
+ {(function (): TranslatedString {
+ switch (config.address_type) {
+ case "email":
+ return i18n.str`Send email`;
+ case "phone":
+ return i18n.str`Send SMS`;
+ }
+ })()}
</Button>
</div>
) : (
@@ -244,11 +450,18 @@ export function AskChallenge({
class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
handler={onSend}
>
- <i18n.Translate>Change email</i18n.Translate>
+ {(function (): TranslatedString {
+ switch (config.address_type) {
+ case "email":
+ return i18n.str`Change email`;
+ case "phone":
+ return i18n.str`Change phone`;
+ }
+ })()}
</Button>
</div>
)}
- </form>
+ </div>
</div>
</Fragment>
);
diff --git a/packages/challenger-ui/src/pages/CallengeCompleted.tsx b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
index f8cd7ce60..bff7b68f5 100644
--- a/packages/challenger-ui/src/pages/CallengeCompleted.tsx
+++ b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
@@ -13,14 +13,32 @@
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, h } from "preact";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionState } from "../hooks/session.js";
+import { useEffect } from "preact/hooks";
-type Props = {
- nonce: string;
-}
-export function CallengeCompleted({nonce}:Props):VNode {
+export function CallengeCompleted(): VNode {
+ const { state } = useSessionState();
+
+ const { i18n } = useTranslationContext();
- return <div>
- completed {nonce}
- </div>
-} \ No newline at end of file
+ useEffect(() => {
+ window.location.href = state?.completedURL ?? "#"
+ },[])
+
+ return (
+ <div class="m-4">
+ <Attention
+ title={i18n.str`Challenge completed`}
+ type="success"
+ >
+ <i18n.Translate>
+ You will be redirected to <a href={state?.completedURL} class="break-all">&quot;
+ {state?.completedURL}
+ &quot;</a>
+ </i18n.Translate>
+ </Attention>
+ </div>
+ );
+}
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
index 612eced0b..7f81b9d77 100644
--- a/packages/challenger-ui/src/pages/Frame.tsx
+++ b/packages/challenger-ui/src/pages/Frame.tsx
@@ -14,56 +14,121 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../context/preferences.js";
+import { useSettingsContext } from "../context/settings.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
export function Frame({ children }: { children: ComponentChildren }): VNode {
+ const settings = useSettingsContext();
+ const [preferences, updatePreferences] = usePreferences();
+
+ const [error, resetError] = useErrorBoundary();
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ resetError();
+ }
+ }, [error]);
+
return (
- <Fragment>
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg">
- <a href="#">
- <img
- class="h-8 w-auto"
- src='data:image/svg+xml,<?xml version="1.0" encoding="UTF-8" standalone="no"?>%0A<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">%0A <g fill="%230042b3" fill-rule="evenodd" stroke-width=".3">%0A <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" />%0A <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" />%0A <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" />%0A </g>%0A <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" />%0A</svg>'
- alt="GNU Taler"
- style="height: 1.5rem; margin: 0.5rem;"
- />
- </a>
- </div>
- <span class="flex items-center text-white text-lg font-bold ml-4">
- Challenger
- </span>
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <Header
+ title="Challenger"
+ onLogout={undefined}
+ iconLinkURL="#"
+ sites={preferences.showChallangeSetup ? [
+ ["New challenge","#/setup/1"]
+ ] :[]}
+ supportedLangs={["en"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
</div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end"></div>
+ <ul role="list" class="space-y-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`${set} switch`}
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+
+ <div class="fixed z-20 top-14 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
</div>
- </header>
+ </div>
<main class="flex-1">{children}</main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">
- Learn more about{" "}
- <a
- target="_blank"
- rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400"
- href="https://taler.net"
- >
- GNU Taler
- </a>
- </p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">
- Copyright © 2014—2023 Taler Systems SA.{" "}
- </p>
- </div>
- </footer>
- </Fragment>
+
+ <Footer
+ testingUrlKey="challenger-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
);
}
diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx
index f431835aa..c7395f605 100644
--- a/packages/challenger-ui/src/pages/Setup.tsx
+++ b/packages/challenger-ui/src/pages/Setup.tsx
@@ -13,47 +13,84 @@
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 { AccessToken, HttpStatusCode, encodeCrock, randomBytes } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ createClientSecretAccessToken,
+ createRFC8959AccessTokenEncoded,
+ encodeCrock,
+ randomBytes,
+} from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
+ ShowInputErrorLabel,
useChallengerApiContext,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { safeToURL } from "../Routing.js";
import { useSessionState } from "../hooks/session.js";
+import { doAutoFocus, undefinedIfEmpty } from "./AnswerChallenge.js";
type Props = {
clientId: string;
- onCreated: (nonce:string) => void;
+ secret: string | undefined;
+ redirectURL: URL | undefined;
+ onCreated: () => void;
+ focus?: boolean;
};
-export function Setup({ clientId, onCreated }: Props): VNode {
+export function Setup({
+ clientId,
+ secret,
+ redirectURL,
+ focus,
+ onCreated,
+}: Props): VNode {
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const { lib } = useChallengerApiContext();
const { start } = useSessionState();
+ const [password, setPassword] = useState<string | undefined>(secret);
+ const [url, setUrl] = useState<string | undefined>(redirectURL?.href);
- const onStart = withErrorHandler(
- async () => {
- return lib.challenger.setup(clientId, "secret-token:chal-secret" as AccessToken);
- },
- (ok) => {
- start({
- clientId,
- redirectURL: "http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet",
- state: encodeCrock(randomBytes(32)),
- });
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ url: !url
+ ? i18n.str`required`
+ : !safeToURL(url)
+ ? i18n.str`invalid format`
+ : undefined,
+ });
- onCreated(ok.body.nonce);
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.NotFound:
- return i18n.str`Client doesn't exist.`;
- }
- },
- );
+ const onStart =
+ !!errors || password === undefined || url === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.setup(
+ clientId,
+ createRFC8959AccessTokenEncoded(password),
+ );
+ },
+ (ok) => {
+ start({
+ nonce: ok.body.nonce,
+ clientId,
+ redirectURL: url,
+ state: encodeCrock(randomBytes(32)),
+ });
+
+ onCreated();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.NotFound:
+ return i18n.str`Client doesn't exist.`;
+ }
+ },
+ );
return (
<Fragment>
@@ -67,15 +104,81 @@ export function Setup({ clientId, onCreated }: Props): VNode {
</i18n.Translate>
</h2>
</div>
- <div class="mt-10">
- <Button
- type="submit"
- class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- handler={onStart}
+
+ <form
+ method="POST"
+ class="mx-auto mt-4 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
>
- <i18n.Translate>Start</i18n.Translate>
- </Button>
+ <i18n.Translate>Password</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ ref={focus ? doAutoFocus : undefined}
+ maxLength={512}
+ autocomplete="password"
+ value={password}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value);
+ }}
+ readOnly={secret !== undefined}
+ class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
</div>
+
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Redirect URL</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="text"
+ name="redirect_url"
+ id="redirect_url"
+ maxLength={512}
+ autocomplete="redirect_url"
+ value={url}
+ onChange={(e) => {
+ setUrl(e.currentTarget.value);
+ }}
+ readOnly={redirectURL !== undefined}
+ class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.url}
+ isDirty={url !== undefined}
+ />
+ </div>
+ </div>
+ </form>
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onStart}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onStart}
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </Button>
+ </div>
</div>
</Fragment>
);
diff --git a/packages/challenger-ui/src/validation-unknown.html b/packages/challenger-ui/src/validation-unknown.html
deleted file mode 100644
index 56c8d156c..000000000
--- a/packages/challenger-ui/src/validation-unknown.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!--
- This file is part of GNU Taler
- (C) 2021--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
--->
-<!DOCTYPE html>
-<html lang="en" class="h-full">
-
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <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" />
- <link rel="stylesheet" href="main.css" />
- <script type="module" src="main.js"></script>
- <title>Validation process unknown (#{{ec}})</title>
-</head>
-
-<body class="min-h-full flex flex-col">
- <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
- <div class="flex flex-row h-16 items-center ">
- <div class="flex px-2 justify-start">
- <div class="flex-shrink-0 bg-white rounded-lg"><a href="#"><img class="h-8 w-auto"
- src="data:image/svg+xml,<?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?>%0A<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 201 90&quot;>%0A <g fill=&quot;%230042b3&quot; fill-rule=&quot;evenodd&quot; stroke-width=&quot;.3&quot;>%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A <path d=&quot;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&quot; />%0A </g>%0A <path d=&quot;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&quot; />%0A</svg>"
- alt="GNU Taler" style="height: 1.5rem; margin: 0.5rem;"></a></div><span
- class="flex items-center text-white text-lg font-bold ml-4">Challenger</span>
- </div>
- <div class="block flex-1 ml-6 "></div>
- <div class="flex justify-end">
- </div>
- </div>
- </header>
-
- <main class="flex-1">
-
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 items-center mt-4">
- <div class="rounded-md bg-red-50 p-4 shadow-xl">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor"
- class="w-8 h-8 text-red-400">
- <path fill-rule="evenodd"
- d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">
- Validation error
- </h3>
- <div class="mt-2 text-sm text-red-700">
- <p>{{hint}}</p>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- </main>
-
- <footer class="bottom-4 mb-4">
- <div class="mt-8 mx-8 md:order-1 md:mt-0">
- <div>
- <p class="text-xs leading-5 text-gray-400">Learn more about <a target="_blank" rel="noreferrer noopener"
- class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a></p>
- </div>
- <div style="flex-grow: 1;"></div>
- <p class="text-xs leading-5 text-gray-400">Copyright © 2014—2023 Taler Systems SA. </p>
- </div>
- </footer>
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index ce3123619..02a79db28 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
diff --git a/packages/idb-bridge/src/backends.test.ts b/packages/idb-bridge/src/backends.test.ts
index 684358eac..64133a73b 100644
--- a/packages/idb-bridge/src/backends.test.ts
+++ b/packages/idb-bridge/src/backends.test.ts
@@ -23,24 +23,23 @@
* Imports.
*/
import test from "ava";
+import { MemoryBackend } from "./MemoryBackend.js";
import {
BridgeIDBCursorWithValue,
BridgeIDBDatabase,
BridgeIDBFactory,
BridgeIDBKeyRange,
- BridgeIDBTransaction,
} from "./bridge-idb.js";
+import { promiseFromRequest, promiseFromTransaction } from "./idbpromutil.js";
import {
IDBCursorDirection,
IDBCursorWithValue,
IDBDatabase,
IDBKeyRange,
- IDBRequest,
IDBValidKey,
} from "./idbtypes.js";
import { initTestIndexedDB, useTestIndexedDb } from "./testingdb.js";
-import { MemoryBackend } from "./MemoryBackend.js";
-import { promiseFromRequest, promiseFromTransaction } from "./idbpromutil.js";
+import { promiseForTransaction } from "./idb-wpt-ported/wptsupport.js";
test.before("test DB initialization", initTestIndexedDB);
@@ -464,8 +463,7 @@ test("export", async (t) => {
const exportedData2 = backend2.exportDump();
t.assert(
- exportedData.databases[dbname].objectStores["books"].records.length ===
- 3,
+ exportedData.databases[dbname].objectStores["books"].records.length === 3,
);
t.deepEqual(exportedData, exportedData2);
@@ -738,3 +736,44 @@ test("range queries", async (t) => {
t.pass();
});
+
+test("idb: multiple transactions", async (t) => {
+ const idb = useTestIndexedDb();
+ const dbname = "mtx-" + new Date().getTime() + Math.random();
+
+ const request = idb.open(dbname);
+ request.onupgradeneeded = () => {
+ const db = request.result as BridgeIDBDatabase;
+ const store = db.createObjectStore("books", { keyPath: "isbn" });
+ const titleIndex = store.createIndex("by_title", "title", { unique: true });
+ const authorIndex = store.createIndex("by_author", "author");
+
+ // Populate with initial data.
+ store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 });
+ store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
+ store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ const tx1 = db.transaction(["books"], "readwrite");
+
+ tx1.oncomplete = () => {
+ t.log("first tx completed");
+ };
+
+ tx1.onerror = () => {
+ t.log("first tx errored");
+ };
+
+ tx1.abort();
+
+ const tx2 = db.transaction(["books"], "readwrite");
+ tx2.commit();
+
+ const tx3 = db.transaction(["books"], "readwrite");
+
+ await promiseForTransaction(t, tx3);
+
+ t.pass();
+});
diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts
index afb3f4224..15c68c733 100644
--- a/packages/idb-bridge/src/bridge-idb.ts
+++ b/packages/idb-bridge/src/bridge-idb.ts
@@ -2730,16 +2730,24 @@ export class BridgeIDBTransaction
if (BridgeIDBFactory.enableTracing) {
console.log("beginning backend transaction to process operation");
}
- this._backendTransaction = await this._backend.beginTransaction(
+ const newBackendTx = await this._backend.beginTransaction(
this._db._backendConnection,
Array.from(this._scope),
this.mode,
);
if (BridgeIDBFactory.enableTracing) {
console.log(
- `started backend transaction (${this._backendTransaction.transactionCookie})`,
+ `started backend transaction (${newBackendTx.transactionCookie})`,
);
}
+ if (this._aborted) {
+ // Probably there is a more elegant way to do this by aborting the
+ // beginTransaction call when the transaction was aborted.
+ // That would require changing the DB backend API.
+ this._backend.rollback(newBackendTx);
+ } else {
+ this._backendTransaction = newBackendTx;
+ }
}
if (!request._source) {
diff --git a/packages/idb-bridge/src/util/errors.ts b/packages/idb-bridge/src/util/errors.ts
index 6c8f81811..57fa46f96 100644
--- a/packages/idb-bridge/src/util/errors.ts
+++ b/packages/idb-bridge/src/util/errors.ts
@@ -41,7 +41,7 @@ const messages = {
export class AbortError extends Error {
constructor(message = messages.AbortError) {
super();
- Object.setPrototypeOf(this, ConstraintError.prototype);
+ Object.setPrototypeOf(this, AbortError.prototype);
this.name = "AbortError";
this.message = message;
}
diff --git a/packages/kyc-ui/.gitignore b/packages/kyc-ui/.gitignore
new file mode 100644
index 000000000..30cb2774c
--- /dev/null
+++ b/packages/kyc-ui/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+/build
+/*.log
+/demobank-ui-settings.js
diff --git a/packages/kyc-ui/Makefile b/packages/kyc-ui/Makefile
new file mode 100644
index 000000000..64f9f83d1
--- /dev/null
+++ b/packages/kyc-ui/Makefile
@@ -0,0 +1,36 @@
+# 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/aml-backoffice-ui
+
+.PHONY: install-nodeps
+install-nodeps:
+ install -d $(spa_dir)
+ install ./dist/prod/* $(spa_dir)
+
+.PHONY: deps
+deps:
+ pnpm install --frozen-lockfile --filter @gnu-taler/aml-backoffice-ui...
+ pnpm run check
+ pnpm run build
+
+.PHONY: install
+install:
+ $(MAKE) deps
+ $(MAKE) install-nodeps
+
diff --git a/packages/kyc-ui/README.md b/packages/kyc-ui/README.md
new file mode 100644
index 000000000..d876aaf2b
--- /dev/null
+++ b/packages/kyc-ui/README.md
@@ -0,0 +1,4 @@
+# KYC UI
+
+Web-based user interface for the exchange kyc.
+
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/kyc-ui/build.mjs
index 3336c53a4..cd3825dc9 100644..100755
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
+++ b/packages/kyc-ui/build.mjs
@@ -1,6 +1,7 @@
+#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -14,15 +15,19 @@
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";
+import { build } from "@gnu-taler/web-util/build";
-export default {
- title: "Pages/Accounts/Create",
- component: TestedComponent,
-};
+await build({
+ type: "production",
+ source: {
+ js: ["src/index.tsx"],
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ ]
+ }],
+ },
+ destination: "./dist/prod",
+ css: "postcss",
+});
diff --git a/packages/auditor-backoffice-ui/src/components/index.stories.ts b/packages/kyc-ui/copyleft-header.js
index c57ddab14..7fa276bea 100644
--- a/packages/auditor-backoffice-ui/src/components/index.stories.ts
+++ b/packages/kyc-ui/copyleft-header.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -13,5 +13,3 @@
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/paths/instance/templates/update/Update.stories.tsx b/packages/kyc-ui/dev.mjs
index 8d07cb31f..2d7e564e8 100644..100755
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
+++ b/packages/kyc-ui/dev.mjs
@@ -1,6 +1,7 @@
+#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -14,19 +15,32 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { serve } from "@gnu-taler/web-util/node";
+import { initializeDev } from "@gnu-taler/web-util/build";
-import { h, VNode, FunctionalComponent } from "preact";
-import { UpdatePage as TestedComponent } from "./UpdatePage.js";
+const devEntryPoints = ["src/index.tsx"];
-export default {
- title: "Pages/Templates/Update",
- component: TestedComponent,
- argTypes: {
- onUpdate: { action: "onUpdate" },
- onBack: { action: "onBack" },
+const build = initializeDev({
+ type: "development",
+ source: {
+ js: devEntryPoints,
+ assets: [{
+ base: "src",
+ files: [
+ "src/index.html",
+ ]
+ }],
},
-};
+ destination: "./dist/dev",
+ public: "/app",
+ css: "postcss",
+});
+
+await build();
+
+serve({
+ folder: "./dist/dev",
+ port: 8080,
+ source: "./src",
+ onSourceUpdate: build,
+});
diff --git a/packages/kyc-ui/package.json b/packages/kyc-ui/package.json
new file mode 100644
index 000000000..b9bd95260
--- /dev/null
+++ b/packages/kyc-ui/package.json
@@ -0,0 +1,63 @@
+{
+ "private": true,
+ "name": "@gnu-taler/kyc-ui",
+ "version": "0.13.4",
+ "author": "sebasjm",
+ "license": "AGPL-3.0-OR-LATER",
+ "description": "UI for GNU Exchange KYC.",
+ "type": "module",
+ "scripts": {
+ "check": "tsc",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
+ "pretty": "prettier --write src"
+ },
+ "eslintConfig": {
+ "plugins": [
+ "header"
+ ],
+ "rules": {
+ "header/header": [
+ 2,
+ "copyleft-header.js"
+ ]
+ },
+ "extends": [
+ "prettier"
+ ]
+ },
+ "devDependencies": {
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@gnu-taler/pogen": "workspace:*",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.17",
+ "autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.9",
+ "mocha": "9.2.0",
+ "po2json": "^0.4.5",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
+ },
+ "pogen": {
+ "domain": "kyc-ui"
+ },
+ "dependencies": {
+ "swr": "2.0.3",
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "preact": "10.11.3"
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/kyc-ui/postcss.config.js
index fdae1a24d..c9a60a43c 100644
--- a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
+++ b/packages/kyc-ui/postcss.config.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -13,6 +13,9 @@
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";
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/kyc-ui/src/Routing.tsx b/packages/kyc-ui/src/Routing.tsx
new file mode 100644
index 000000000..a5508e7da
--- /dev/null
+++ b/packages/kyc-ui/src/Routing.tsx
@@ -0,0 +1,127 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ Loading,
+ urlPattern,
+ useCurrentLocation,
+ useNavigationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import { AccessToken, assertUnreachable } from "@gnu-taler/taler-util";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { CallengeCompleted } from "./pages/CallengeCompleted.js";
+import { Frame } from "./pages/Frame.js";
+import { Start } from "./pages/Start.js";
+import { useSessionState } from "./hooks/session.js";
+
+export function Routing(): VNode {
+ // check session and defined if this is
+ // public routing or private
+ return (
+ <Frame>
+ <PublicRounting />
+ </Frame>
+ );
+}
+
+const publicPages = {
+ completed: urlPattern(/\/completed/, () => `#/completed`),
+ start: urlPattern(/\/start/, () => `#/start`),
+};
+
+function safeGetParam(
+ ps: Record<string, string[]>,
+ n: string,
+): string | undefined {
+ if (!ps[n] || ps[n].length == 0) return undefined;
+ return ps[n][0];
+}
+
+export function safeToURL(s: string | undefined): URL | undefined {
+ if (s === undefined) return undefined;
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+const ACCESS_TOKEN_REGEX = new RegExp("[A-Z0-9]{52}")
+
+/**
+ * by how the exchange
+ * /kyc-spa/KXAFXEWM7E3EJSYD9GJ30FYK1C17AKZWV119ZJA3XGPBBMZFJ2C0
+ *
+ * @returns
+ */
+function getAccessTokenFromURL(): AccessToken | undefined {
+ if (typeof window === "undefined") return undefined;
+ const paths = window.location.pathname.split("/");
+ if (paths.length < 3) return undefined;
+ const res = paths[2] as AccessToken
+ if (!ACCESS_TOKEN_REGEX.test(res)) return undefined;
+ return res;
+}
+
+function PublicRounting(): VNode {
+ const location = useCurrentLocation(publicPages);
+ const { state, start } = useSessionState();
+ const { navigateTo } = useNavigationContext();
+ useErrorBoundary((e) => {
+ console.log("error", e);
+ });
+ const sessionToken = state?.accessToken
+ const urlToken = getAccessTokenFromURL();
+ useEffect(() => {
+ if (!urlToken) {
+ //special case, loading without URL the it should use the session
+ return;
+ }
+ // loading a new session
+ if (urlToken !== sessionToken) {
+ start(urlToken)
+ }
+ },[sessionToken, urlToken])
+
+ if (!sessionToken) {
+ return <div>No access token</div>;
+ }
+
+ switch (location.name) {
+ case undefined: {
+ navigateTo(publicPages.start.url({}));
+ return <Loading />;
+ }
+ case "start": {
+ return (
+ <Start
+ token={sessionToken}
+ onLoggedOut={() => {
+ navigateTo(publicPages.completed.url({}));
+ }}
+ />
+ );
+ }
+
+ case "completed": {
+ return <CallengeCompleted />;
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/kyc-ui/src/app.tsx b/packages/kyc-ui/src/app.tsx
new file mode 100644
index 000000000..e74249f73
--- /dev/null
+++ b/packages/kyc-ui/src/app.tsx
@@ -0,0 +1,169 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ CacheEvictor,
+ TalerExchangeCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ExchangeApiProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+import { SettingsProvider } from "./context/settings.js";
+import { strings } from "./i18n/strings.js";
+import { Frame } from "./pages/Frame.js";
+import { KycUiSettings, fetchSettings } from "./settings.js";
+import { revalidateKycInfo } from "./hooks/kyc.js";
+
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+export function App(): VNode {
+ const [settings, setSettings] = useState<KycUiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ forceLang="en"
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <ExchangeApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={Frame}
+ evictors={{
+ exchange: evictExchangeSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </ExchangeApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("kyc-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
+
+const evictExchangeSwrCache: CacheEvictor<TalerExchangeCacheEviction> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerExchangeCacheEviction.MAKE_AML_DECISION: {
+ return;
+ }
+ case TalerExchangeCacheEviction.UPLOAD_KYC_FORM: {
+ await revalidateKycInfo();
+ return;
+ }
+ default: {
+ assertUnreachable(op);
+ }
+ }
+ },
+};
diff --git a/packages/kyc-ui/src/assets/home.svg b/packages/kyc-ui/src/assets/home.svg
new file mode 100644
index 000000000..35f340162
--- /dev/null
+++ b/packages/kyc-ui/src/assets/home.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+</svg> \ No newline at end of file
diff --git a/packages/kyc-ui/src/assets/logo-2021.svg b/packages/kyc-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/kyc-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/kyc-ui/src/assets/people.svg b/packages/kyc-ui/src/assets/people.svg
new file mode 100644
index 000000000..1dc878b81
--- /dev/null
+++ b/packages/kyc-ui/src/assets/people.svg
@@ -0,0 +1,3 @@
+<svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
+</svg> \ No newline at end of file
diff --git a/packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx
new file mode 100644
index 000000000..0e2a90fc1
--- /dev/null
+++ b/packages/kyc-ui/src/components/ErrorLoadingWithDebug.tsx
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 { TalerError } from "@gnu-taler/taler-util";
+import { ErrorLoading } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { usePreferences } from "../context/preferences.js";
+
+export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode {
+ const [pref] = usePreferences();
+ return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />;
+}
diff --git a/packages/kyc-ui/src/context/preferences.ts b/packages/kyc-ui/src/context/preferences.ts
new file mode 100644
index 000000000..3dc4294cb
--- /dev/null
+++ b/packages/kyc-ui/src/context/preferences.ts
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ showDebugInfo: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("showDebugInfo", codecForBoolean())
+ .build("Preferences");
+
+const defaultPreferences: Preferences = {
+ showDebugInfo: false,
+};
+
+const PREFERENCES_KEY = buildStorageKey(
+ "kyc-preferences",
+ codecForPreferences(),
+);
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ PREFERENCES_KEY,
+ defaultPreferences,
+ );
+
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "showDebugInfo",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ }
+}
diff --git a/packages/auditor-backoffice-ui/src/context/instance.ts b/packages/kyc-ui/src/context/settings.ts
index 5800ade7e..1b4b6576d 100644
--- a/packages/auditor-backoffice-ui/src/context/instance.ts
+++ b/packages/kyc-ui/src/context/settings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
+ (C) 2022-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
@@ -14,23 +14,31 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { KycUiSettings } from "../settings.js";
+
/**
*
* @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);
+export type Type = KycUiSettings;
+
+const initial: KycUiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: KycUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/kyc-ui/src/declaration.d.ts b/packages/kyc-ui/src/declaration.d.ts
new file mode 100644
index 000000000..581cbcd07
--- /dev/null
+++ b/packages/kyc-ui/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: string;
+ export default content;
+}
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/kyc-ui/src/forms/index.ts b/packages/kyc-ui/src/forms/index.ts
new file mode 100644
index 000000000..a8803ec9f
--- /dev/null
+++ b/packages/kyc-ui/src/forms/index.ts
@@ -0,0 +1,45 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ FormMetadata,
+ InternationalizationAPI,
+} from "@gnu-taler/web-util/browser";
+import { simplest } from "./simplest.js";
+import { personalInfo } from "./personal-info.js";
+import { nameAndDob } from "./nameAndBirthdate.js";
+
+export const preloadedForms: (
+ i18n: InternationalizationAPI,
+) => Array<FormMetadata> = (i18n) => [
+ {
+ label: i18n.str`Simple comment`,
+ id: "__simple_comment",
+ version: 1,
+ config: simplest(i18n),
+ },
+ {
+ label: i18n.str`Personal info`,
+ id: "personal-info",
+ version: 1,
+ config: personalInfo(i18n),
+ },
+ {
+ label: i18n.str`Name and birthdate`,
+ id: "name_and_dob",
+ version: 1,
+ config: nameAndDob(i18n),
+ },
+];
diff --git a/packages/kyc-ui/src/forms/nameAndBirthdate.ts b/packages/kyc-ui/src/forms/nameAndBirthdate.ts
new file mode 100644
index 000000000..dc4aa52e5
--- /dev/null
+++ b/packages/kyc-ui/src/forms/nameAndBirthdate.ts
@@ -0,0 +1,44 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 type {
+ DoubleColumnForm,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+
+export const nameAndDob = (
+ i18n: InternationalizationAPI,
+): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: "full_name" as UIHandlerId,
+ label: i18n.str`Full Name`,
+ },
+ {
+ type: "textArea",
+ id: "birthdate" as UIHandlerId,
+ label: i18n.str`Birthdate`,
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/kyc-ui/src/forms/personal-info.ts b/packages/kyc-ui/src/forms/personal-info.ts
new file mode 100644
index 000000000..799a8eb3a
--- /dev/null
+++ b/packages/kyc-ui/src/forms/personal-info.ts
@@ -0,0 +1,66 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 type {
+ DoubleColumnForm,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+
+export const personalInfo = (
+ i18n: InternationalizationAPI,
+): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ // {
+ // type: "absoluteTimeText",
+ // name: "dateOfDeath",
+ // label: i18n.str`Date of death`,
+ // pattern: "dd/MM/yyyy",
+ // // help: i18n.str`if deceased. format 'dd/MM/yyyy'`,
+ // help: i18n.str`if deceased'`,
+ // id: ".birthdate" as UIHandlerId,
+ // },
+ {
+ type: "choiceStacked",
+ id: "trucker" as UIHandlerId,
+ required: true,
+ label: i18n.str`Are you a cross-border truck driver?`,
+ choices: [
+ {
+ label: i18n.str`Yes`,
+ value: "yes",
+ },
+ {
+ label: i18n.str`No`,
+ value: "no",
+ },
+ ],
+ },
+ {
+ type: "amount",
+ id: "money" as UIHandlerId,
+ currency: "YEIN",
+ converterId: "Taler.Amount",
+ label: i18n.str`How much is in your pockets?`,
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/kyc-ui/src/forms/simplest.ts b/packages/kyc-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..9cadbc95c
--- /dev/null
+++ b/packages/kyc-ui/src/forms/simplest.ts
@@ -0,0 +1,76 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 type {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+
+export const simplest = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
+ design: [
+ {
+ title: i18n.str`Simple form`,
+ fields: [
+ {
+ type: "textArea",
+ id: "comment" as UIHandlerId,
+ label: i18n.str`Comment`,
+ },
+ ],
+ },
+ resolutionSection(i18n),
+ ],
+});
+
+export function resolutionSection(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormSection {
+ return {
+ title: i18n.str`Resolution`,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ id: "state" as UIHandlerId,
+ label: i18n.str`New state`,
+ converterId: "TalerExchangeApi.AmlState",
+ choices: [
+ {
+ value: "frozen",
+ label: i18n.str`Frozen`,
+ },
+ {
+ value: "pending",
+ label: i18n.str`Pending`,
+ },
+ {
+ value: "normal",
+ label: i18n.str`Normal`,
+ },
+ ],
+ },
+ {
+ type: "amount",
+ id: "threshold" as UIHandlerId,
+ currency: "NETZBON",
+ converterId: "Taler.Amount",
+ label: i18n.str`New threshold`,
+ },
+ ],
+ };
+}
diff --git a/packages/kyc-ui/src/hooks/form.ts b/packages/kyc-ui/src/hooks/form.ts
new file mode 100644
index 000000000..452deabaf
--- /dev/null
+++ b/packages/kyc-ui/src/hooks/form.ts
@@ -0,0 +1,227 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AbsoluteTime,
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ UIFieldHandler,
+ UIFormElementConfig,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { undefinedIfEmpty } from "../pages/Start.js";
+
+// export type UIField = {
+// value: string | undefined;
+// onUpdate: (s: string) => void;
+// error: TranslatedString | undefined;
+// };
+
+export type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIFieldHandler
+ : T[k] extends AmountJson
+ ? UIFieldHandler
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIFieldHandler
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : T[k] extends AbsoluteTime
+ ? TranslatedString
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ shape: Array<UIHandlerId>,
+ form: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<FormValues<T>>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const handler = shape.reduce((handleForm, fieldId) => {
+ const path = fieldId.split(".");
+
+ function updater(newValue: unknown) {
+ updateForm(setValueDeeper(form, path, newValue));
+ }
+
+ const currentValue = getValueDeeper<string>(form as any, path, undefined);
+ const currentError = getValueDeeper<TranslatedString>(
+ errors as any,
+ path,
+ undefined,
+ );
+ const field: UIFieldHandler = {
+ error: currentError,
+ value: currentValue,
+ onChange: updater,
+ state: {}, //FIXME: add the state of the field (hidden, )
+ };
+
+ return setValueDeeper(handleForm, path, field);
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ shape: Array<UIHandlerId>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] =
+ useState<RecursivePartial<FormValues<T>>>(defaultValue);
+
+ const status = check(form);
+ const handler = constructFormHandler(shape, form, updateForm, status.errors);
+
+ return [handler, status];
+}
+
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
+export function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue;
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+export function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (!head) {
+ return setValueDeeper(object, rest, value);
+ }
+ if (object === undefined) {
+ return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ }
+ return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+}
+
+export function getShapeFromFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getShapeFromFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+
+export function getRequiredFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getRequiredFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+export function validateRequiredFields<FormType>(
+ errors: FormErrors<FormType> | undefined,
+ form: object,
+ fields: Array<UIHandlerId>,
+): FormErrors<FormType> | undefined {
+ let result: FormErrors<FormType> | undefined = errors;
+ fields.forEach((f) => {
+ const path = f.split(".");
+ const v = getValueDeeper(form as any, path);
+ result = setValueDeeper(result, path, !v ? "required" : undefined);
+ });
+ return result;
+}
diff --git a/packages/kyc-ui/src/hooks/kyc.ts b/packages/kyc-ui/src/hooks/kyc.ts
new file mode 100644
index 000000000..38d9b543d
--- /dev/null
+++ b/packages/kyc-ui/src/hooks/kyc.ts
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AccessToken,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
+import _useSWR, { SWRHook, mutate } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateKycInfo() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "checkKycInfo",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useKycInfo(token: AccessToken) {
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ async function fetcher([ac]: [AccessToken]) {
+ return await api.checkKycInfo(ac, [], { timeout: 1000 });
+ }
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"checkKycInfo">,
+ TalerHttpError
+ >([token, "checkKycInfo"], fetcher, {
+ revalidateIfStale: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/kyc-ui/src/hooks/session.ts b/packages/kyc-ui/src/hooks/session.ts
new file mode 100644
index 000000000..f7d903bcb
--- /dev/null
+++ b/packages/kyc-ui/src/hooks/session.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AccessToken,
+ Codec,
+ buildCodecForObject,
+ codecForAccessToken,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+
+export type SessionState = {
+ accessToken: AccessToken;
+};
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForObject<SessionState>()
+ .property("accessToken", codecForAccessToken())
+ // .property("lastAddress", codecOptional(codecForList(codecForLastAddress())))
+ .build("SessionState");
+
+export interface SessionStateHandler {
+ state: SessionState | undefined;
+ start(s: AccessToken): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "kyc-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(SESSION_STATE_KEY);
+
+ return {
+ state,
+ start(accessToken) {
+ update({accessToken})
+ },
+ };
+}
diff --git a/packages/kyc-ui/src/i18n/challenger-ui.pot b/packages/kyc-ui/src/i18n/challenger-ui.pot
new file mode 100644
index 000000000..494fbfb8f
--- /dev/null
+++ b/packages/kyc-ui/src/i18n/challenger-ui.pot
@@ -0,0 +1,199 @@
+# This file is part of GNU Taler
+# (C) 2022-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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Kyc UI\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/pages/AnswerChallenge.tsx:55
+#, c-format
+msgid "Can't be empty"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:81
+#, c-format
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:108
+#, c-format
+msgid "Invalid request"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:111
+#, c-format
+msgid "Invalid pin"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:142
+#, c-format
+msgid "Please enter the TAN you received to authenticate."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:147
+#, c-format
+msgid "A TAN was sent to your address &quot;%1$s&quot;."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:151
+#, c-format
+msgid ""
+"We recently already sent a TAN to your address &quot; %1$s&quot;. A new TAN will "
+"not be transmitted again before %2$s."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:161
+#, c-format
+msgid "You can try another PIN but just %1$s times more."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:181
+#, c-format
+msgid "TAN code"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:204
+#, c-format
+msgid "You have %1$s attempts left."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:217
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:227
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:76
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:84
+#, c-format
+msgid "invalid email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:86
+#, c-format
+msgid "emails don't match"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:130
+#, c-format
+msgid "Enter contact details"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:133
+#, c-format
+msgid ""
+"You will receive an email with a TAN code that must be provided on the next "
+"page."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:152
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:180
+#, c-format
+msgid "Repeat email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:198
+#, c-format
+msgid "You can change your email address another %1$s times."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:211
+#, c-format
+msgid "Send email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:237
+#, c-format
+msgid "Bad request"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:238
+#, c-format
+msgid "Could not start the challenge, check configuration."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:246
+#, c-format
+msgid "Not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:247
+#, c-format
+msgid "Nonce not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:253
+#, c-format
+msgid "Not acceptable"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:254
+#, c-format
+msgid "Server has wrong template configuration"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:262
+#, c-format
+msgid "Internal error"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:263
+#, c-format
+msgid "Check logs"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:33
+#, c-format
+msgid "The URL is wrong"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:36
+#, c-format
+msgid "Maybe the validation check expired."
+msgstr ""
+
+#: src/pages/Setup.tsx:53
+#, c-format
+msgid "Client doesn't exist."
+msgstr ""
+
+#: src/pages/Setup.tsx:65
+#, c-format
+msgid "Setup new challenge with client ID: &quot;%1$s&quot;"
+msgstr ""
+
+#: src/pages/Setup.tsx:76
+#, c-format
+msgid "Start"
+msgstr ""
+
diff --git a/packages/kyc-ui/src/i18n/poheader b/packages/kyc-ui/src/i18n/poheader
new file mode 100644
index 000000000..ed65f25e2
--- /dev/null
+++ b/packages/kyc-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# This file is part of GNU Taler
+# (C) 2022-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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Kyc UI\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/kyc-ui/src/i18n/strings.ts b/packages/kyc-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..ea13fed2e
--- /dev/null
+++ b/packages/kyc-ui/src/i18n/strings.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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/>
+ */
+export interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "en",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/kyc-ui/src/index.html b/packages/kyc-ui/src/index.html
new file mode 100644
index 000000000..cbbf6addf
--- /dev/null
+++ b/packages/kyc-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--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
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <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>KYC</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/kyc-ui/src/index.tsx
index 719f99209..f559288a3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx
+++ b/packages/kyc-ui/src/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
+ (C) 2022-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
@@ -14,13 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
-import { h, VNode } from "preact";
+const app = document.getElementById("app");
-export default function UpdateTransfer(): VNode {
- return <div>order transfer page</div>;
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
}
diff --git a/packages/kyc-ui/src/pages/CallengeCompleted.tsx b/packages/kyc-ui/src/pages/CallengeCompleted.tsx
new file mode 100644
index 000000000..3619dba79
--- /dev/null
+++ b/packages/kyc-ui/src/pages/CallengeCompleted.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+export function CallengeCompleted(): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="m-4">
+ <Attention title={i18n.str`Kyc completed`} type="success">
+ <i18n.Translate>You will be redirected to nowhere</i18n.Translate>
+ </Attention>
+ </div>
+ );
+}
diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx
new file mode 100644
index 000000000..157ca3406
--- /dev/null
+++ b/packages/kyc-ui/src/pages/FillForm.tsx
@@ -0,0 +1,276 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AbsoluteTime,
+ AccessToken,
+ Amounts,
+ HttpStatusCode,
+ KycRequirementInformation,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Button,
+ FormMetadata,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
+ UIFormElementConfig,
+ UIHandlerId,
+ convertUiField,
+ getConverterById,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { preloadedForms } from "../forms/index.js";
+import {
+ FormErrors,
+ useFormState,
+ validateRequiredFields,
+} from "../hooks/form.js";
+import { undefinedIfEmpty } from "./Start.js";
+
+type Props = {
+ token: AccessToken;
+ formId: string;
+ requirement: KycRequirementInformation;
+ onComplete: () => void;
+};
+
+type FormType = {
+ // state: TalerExchangeApi.AmlState;
+};
+
+type KycFormMetadata = {
+ id: string;
+ version: number;
+ when: AbsoluteTime;
+};
+
+type KycForm = {
+ header: KycFormMetadata;
+ payload: object;
+};
+
+export function FillForm({
+ token,
+ formId,
+ requirement,
+ onComplete,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const { config, lib } = useExchangeApiContext();
+ // const { forms } = useUiFormsContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const theForm = searchForm(i18n, [], formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+ const reqId = requirement.id;
+ if (!reqId) {
+ return <div>no id for this form, can't upload</div>;
+ }
+ const shape: Array<UIHandlerId> = [];
+ const requiredFields: Array<UIHandlerId> = [];
+
+ theForm.config.design.forEach((section) => {
+ Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
+ Array.prototype.push.apply(
+ requiredFields,
+ getRequiredFields(section.fields),
+ );
+ });
+ const [form, state] = useFormState<FormType>(shape, {}, (st) => {
+ const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({});
+
+ const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
+ validateRequiredFields(partialErrors, st, requiredFields),
+ );
+
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ }
+
+ return {
+ status: "fail",
+ result: st as any,
+ errors,
+ };
+ });
+ const validatedForm = state.status !== "ok" ? undefined : state.result;
+
+ const submitHandler =
+ validatedForm === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ const information: KycForm = {
+ header: {
+ id: theForm.id,
+ version: theForm.version,
+ when: AbsoluteTime.now(),
+ },
+ payload: validatedForm,
+ };
+
+ // const data = new FormData()
+ // data.set("header", JSON.stringify(information.header))
+ // data.set("payload", JSON.stringify(information.payload))
+ const data = new URLSearchParams()
+ Object.entries(validatedForm as Record<string,string>).forEach(([key, value]) => {
+ if (key === "money") {
+ data.set(key,Amounts.stringify(value))
+ } else {
+ data.set(key,(value))
+ }
+ })
+ return lib.exchange.uploadKycForm(reqId, data);
+ },
+ (res) => {
+ onComplete();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.PayloadTooLarge:
+ return i18n.str`The form is too big for the server, try uploading smaller files.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`The account was not found`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`Officer disabled or more recent decision was already submitted.`;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+
+ return (
+ <div class="rounded-lg bg-white px-5 py-6 shadow m-4">
+ <LocalNotificationBanner notification={notification} />
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {theForm.config.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={convertUiField(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <button
+ onClick={onComplete}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <Button
+ type="submit"
+ handler={submitHandler}
+ // disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(shape, getRequiredFields(field.fields));
+ }
+ });
+ return shape;
+}
+function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
+ }
+ });
+ return shape;
+}
+function searchForm(
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
+ formId: string,
+): FormMetadata | undefined {
+ {
+ const found = forms.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ {
+ const pf = preloadedForms(i18n);
+ const found = pf.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ return undefined;
+}
diff --git a/packages/kyc-ui/src/pages/Frame.tsx b/packages/kyc-ui/src/pages/Frame.tsx
new file mode 100644
index 000000000..a45fd129b
--- /dev/null
+++ b/packages/kyc-ui/src/pages/Frame.tsx
@@ -0,0 +1,132 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../context/preferences.js";
+import { useSettingsContext } from "../context/settings.js";
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+export function Frame({ children }: { children: ComponentChildren }): VNode {
+ const settings = useSettingsContext();
+ const [preferences, updatePreferences] = usePreferences();
+
+ const [error, resetError] = useErrorBoundary();
+ const { i18n } = useTranslationContext();
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ resetError();
+ }
+ }, [error]);
+
+ return (
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <Header
+ title="KYC"
+ onLogout={undefined}
+ iconLinkURL="#"
+ sites={[]}
+ supportedLangs={["en"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ name={`${set} switch`}
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+
+ <div class="fixed z-20 top-14 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
+ </div>
+ </div>
+
+ <main class="flex-1">{children}</main>
+
+ <Footer
+ testingUrlKey="kyc-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts b/packages/kyc-ui/src/pages/MissingParams.tsx
index 1d8c76ff9..5eb1e434e 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
+++ b/packages/kyc-ui/src/pages/MissingParams.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -13,7 +13,10 @@
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, h } from "preact";
-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";
+export function MissingParams() :VNode {
+ return <div>
+ missing params: {window.location.href}
+ </div>
+} \ No newline at end of file
diff --git a/packages/kyc-ui/src/pages/NonceNotFound.tsx b/packages/kyc-ui/src/pages/NonceNotFound.tsx
new file mode 100644
index 000000000..16b3d90ef
--- /dev/null
+++ b/packages/kyc-ui/src/pages/NonceNotFound.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+type Form = {
+ email: string;
+};
+
+export function NonceNotFound(): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>The URL is wrong</i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>Maybe the validation check expired.</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx
new file mode 100644
index 000000000..f84e3e77a
--- /dev/null
+++ b/packages/kyc-ui/src/pages/Start.tsx
@@ -0,0 +1,404 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ AccessToken,
+ HttpStatusCode,
+ KycRequirementInformation,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ Loading,
+ LocalNotificationBanner,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useKycInfo } from "../hooks/kyc.js";
+import { FillForm } from "./FillForm.js";
+
+type Props = {
+ onLoggedOut: () => void;
+ token: AccessToken;
+};
+
+function ShowReqList({
+ token,
+ onFormSelected,
+}: {
+ token: AccessToken;
+ onFormSelected: (r: KycRequirementInformation) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ // const { lib } = useExchangeApiContext();
+ // const { state, start } = useSessionState();
+ const result = useKycInfo(token);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotModified: {
+ return <div> not modified </div>;
+ }
+ case HttpStatusCode.NoContent: {
+ return <div> not requirements </div>;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ const errors = undefinedIfEmpty({
+ // password: !password ? i18n.str`required` : undefined,
+ // url: !url
+ // ? i18n.str`required`
+ // : !safeToURL(url)
+ // ? i18n.str`invalid format`
+ // : undefined,
+ });
+
+ // const onStart =
+ // !!errors
+ // ? undefined
+ // : withErrorHandler(
+ // async () => {
+ // return {
+ // type: "ok",
+ // body: {},
+ // }
+ // // return lib.exchange.uploadKycForm(
+ // // "clientId",
+ // // createRFC8959AccessTokenEncoded(password),
+ // // );
+ // },
+ // (ok) => {
+ // // start({
+ // // nonce: ok.body.nonce,
+ // // clientId,
+ // // redirectURL: url,
+ // // state: encodeCrock(randomBytes(32)),
+ // // });
+
+ // onCreated();
+ // },
+ // // () => {
+ // // // switch (fail.case) {
+ // // // case HttpStatusCode.NotFound:
+ // // // return i18n.str`Client doesn't exist.`;
+ // // // }
+ // // },
+ // );
+
+ // const requirements: typeof result.body.requirements = [{
+ // description: "this is the form description, click to show the form field bla bla bla",
+ // form: "asdasd" as KycBuiltInFromId,
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "this is the description of the link and service provider.",
+ // form: "LINK",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "you can't click this because this is only information, wait until AML officer replies.",
+ // form: "INFO",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }]
+ const requirements = result.body.requirements;
+ if (!result.body.requirements.length) {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>No requirements for this account</i18n.Translate>
+ </h2>
+ </div>
+ <div class="m-8">
+ <Attention title={i18n.str`Kyc completed`} type="success">
+ <i18n.Translate>You can close this now</i18n.Translate>
+ </Attention>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Complete any of the following requirements
+ </i18n.Translate>
+ </h2>
+ </div>
+
+ <div class="mt-8">
+ <ul
+ role="list"
+ class=" divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
+ >
+ {requirements.map((req, idx) => {
+ return (
+ <li
+ key={idx}
+ class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"
+ >
+ <RequirementRow
+ requirement={req}
+ onFormSelected={() => {
+ onFormSelected(req);
+ }}
+ />
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+export function Start({ token, onLoggedOut }: Props): VNode {
+ const [req, setReq] = useState<KycRequirementInformation>();
+ // if (!state) {
+ // return <Loading />;
+ // }
+
+ if (!req) {
+ return <ShowReqList token={token} onFormSelected={(r) => setReq(r)} />;
+ }
+ return (
+ <FillForm
+ formId={req.form}
+ requirement={req}
+ token={token}
+ onComplete={() => {
+ setReq(undefined);
+ }}
+ />
+ );
+}
+
+function RequirementRow({
+ requirement: req,
+ onFormSelected,
+}: {
+ requirement: KycRequirementInformation;
+ onFormSelected: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { lib } = useExchangeApiContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const reqId = req.id;
+ const startHandler = !reqId
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.exchange.startExternalKycProcess(reqId);
+ },
+ (res) => {
+ window.open(res.body.redirect_url, "_blank");
+ },
+ );
+
+ switch (req.form) {
+ case "INFO": {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Information</i18n.Translate>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+ case "LINK": {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <Button type="submit" handler={startHandler}>
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Begin KYC process</i18n.Translate>
+ </Button>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ <div class="flex shrink-0 items-center gap-x-4">
+ <div class="hidden sm:flex sm:flex-col sm:items-end">
+ <p class="text-sm leading-6 text-gray-900">
+ <i18n.Translate>Start</i18n.Translate>
+ </p>
+ </div>
+ <svg
+ class="h-5 w-5 flex-none text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </Fragment>
+ );
+ }
+ default: {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <button onClick={onFormSelected}>
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Form</i18n.Translate>
+ </button>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ <div class="flex shrink-0 items-center gap-x-4">
+ <div class="hidden sm:flex sm:flex-col sm:items-end">
+ <p class="text-sm leading-6 text-gray-900">Fill form</p>
+ </div>
+ <svg
+ class="h-5 w-5 flex-none text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </Fragment>
+ );
+ }
+ }
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null): void {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
+export function undefinedIfEmpty<T extends object | undefined>(
+ obj: T,
+): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/kyc-ui/src/scss/main.css b/packages/kyc-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/kyc-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/kyc-ui/src/settings.json b/packages/kyc-ui/src/settings.json
new file mode 100644
index 000000000..c43967a16
--- /dev/null
+++ b/packages/kyc-ui/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "backendBaseURL": "http://exchange.taler.test:1180/"
+}
diff --git a/packages/kyc-ui/src/settings.ts b/packages/kyc-ui/src/settings.ts
new file mode 100644
index 000000000..7db986806
--- /dev/null
+++ b/packages/kyc-ui/src/settings.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-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 {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+
+export interface KycUiSettings {
+ // Where exchange backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: KycUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+};
+
+const codecForKycUiSettings = (): Codec<KycUiSettings> =>
+ buildCodecForObject<KycUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .build("codecForKycUiSettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: KycUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForKycUiSettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/kyc-ui/tailwind.config.js
index 6b4b63735..d384690e2 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
+++ b/packages/kyc-ui/tailwind.config.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (C) 2022-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
@@ -13,16 +13,16 @@
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,
+ content: {
+ relative: true,
+ files: [
+ "./src/**/*.{html,tsx}",
+ "./node_modules/@gnu-taler/web-util/src/**/*.{html,tsx}"
+ ],
+ },
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
};
diff --git a/packages/kyc-ui/tsconfig.json b/packages/kyc-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/kyc-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "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",
+ "jsxFragmentFactory": "Fragment",
+ "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. */,
+ /* 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/**/*"]
+}
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index bc8627312..0bb2628bb 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,11 +1,10 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.11.4",
+ "version": "0.13.4",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
- "build": "pnpm compile",
"render-examples": "node dist/test/render-examples.js dist/pages dist/examples",
"lint-check": "eslint '{src,tests}/**/*.{js,jsx,ts,tsx}'",
"lint-fix": "eslint --fix '{src,tests}/**/*.{js,jsx,ts,tsx}'",
diff --git a/packages/merchant-backoffice-ui/dev.mjs b/packages/merchant-backoffice-ui/dev.mjs
index 6cc99add0..278d11a52 100755
--- a/packages/merchant-backoffice-ui/dev.mjs
+++ b/packages/merchant-backoffice-ui/dev.mjs
@@ -24,7 +24,7 @@ const build = initializeDev({
type: "development",
source: {
js: devEntryPoints,
- assets: [{base:"src",files:["src/index.html"]}],
+ assets: [{base:"src",files:["src/index.html","src/settings.json"]}],
},
css: "sass",
destination: "./dist/dev",
diff --git a/packages/merchant-backoffice-ui/error.db b/packages/merchant-backoffice-ui/error.db
new file mode 100644
index 000000000..ee76b3a51
--- /dev/null
+++ b/packages/merchant-backoffice-ui/error.db
@@ -0,0 +1,22 @@
+ JOIN bank_account_transactions AS txs
+ ON bank_transaction=txs.bank_transaction_id
+ WHERE
+ bank_account_id=$1 AND
+ bank_transaction_id > $2
+ ORDER BY bank_transaction_id ASC
+ LIMIT $3
+
+2024-08-01 09:45:45.981 -03 [sebasjm] sebasjm@bank DETAIL: parameters: $1 = '5', $2 = '0', $3 = '1024'
+2024-08-01 09:45:45.981 -03 [sebasjm] sebasjm@bank LOG: duration: 0.011 ms
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DEBUG: bind <unnamed> to lookup_kyc_status
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: duration: 0.057 ms bind lookup_kyc_status: SELECT h_wire,exchange_kyc_serial,payto_uri,exchange_url,kyc_timestamp,kyc_ok FROM merchant_instances JOIN merchant_accounts USING (merchant_serial) JOIN merchant_kyc USING (account_serial) WHERE merchant_instances.merchant_id=$1
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DETAIL: parameters: $1 = 'default'
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: execute lookup_kyc_status: SELECT h_wire,exchange_kyc_serial,payto_uri,exchange_url,kyc_timestamp,kyc_ok FROM merchant_instances JOIN merchant_accounts USING (merchant_serial) JOIN merchant_kyc USING (account_serial) WHERE merchant_instances.merchant_id=$1
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DETAIL: parameters: $1 = 'default'
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: duration: 0.036 ms
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DEBUG: bind <unnamed> to select_accounts
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: duration: 0.028 ms bind select_accounts: SELECT h_wire,salt,payto_uri,credit_facade_url,credit_facade_credentials,active FROM merchant_accounts WHERE merchant_serial= (SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1);
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DETAIL: parameters: $1 = 'default'
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: execute select_accounts: SELECT h_wire,salt,payto_uri,credit_facade_url,credit_facade_credentials,active FROM merchant_accounts WHERE merchant_serial= (SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1);
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant DETAIL: parameters: $1 = 'default'
+2024-08-01 09:45:47.237 -03 [sebasjm] sebasjm@merchant LOG: duration: 0.019 ms
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index 8aabdce87..c802cc25a 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,21 +1,18 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.13.4-dev.1",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
"clean": "rm -rf dist tsconfig.tsbuildinfo",
- "build": "./build.mjs",
"check": "tsc",
"compile": "tsc && ./build.mjs",
- "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
+ "dev": "./dev.mjs",
"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",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
"typedoc": "typedoc --out dist/typedoc ./src/",
"pretty": "prettier --write src"
},
@@ -24,25 +21,20 @@
"@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"
+ "swr": "2.2.2"
},
"devDependencies": {
- "eslint": "^8.56.0",
- "@typescript-eslint/eslint-plugin": "^6.19.0",
- "@typescript-eslint/parser": "^6.19.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-react": "^7.33.2",
"@creativebulma/bulma-tooltip": "^1.2.0",
"@gnu-taler/pogen": "workspace:*",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^8.2.3",
"@types/node": "^18.11.17",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
"base64-inline-loader": "^1.1.1",
"bulma": "^0.9.2",
"bulma-checkbox": "^1.1.1",
@@ -52,13 +44,10 @@
"bulma-timeline": "^3.0.4",
"bulma-upload-control": "^1.2.0",
"chai": "^4.3.6",
- "dotenv": "^8.2.0",
- "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",
+ "eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
"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",
diff --git a/packages/merchant-backoffice-ui/preact.config.js b/packages/merchant-backoffice-ui/preact.config.js
deleted file mode 100644
index b20017a0c..000000000
--- a/packages/merchant-backoffice-ui/preact.config.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- 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 { 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/merchant-backoffice-ui/preact.single-config.js b/packages/merchant-backoffice-ui/preact.single-config.js
deleted file mode 100644
index d3640a5a6..000000000
--- a/packages/merchant-backoffice-ui/preact.single-config.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- 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 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/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 5be21ff8f..43598ce91 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -41,6 +41,7 @@ import { SWRConfig } from "swr";
import { Routing } from "./Routing.js";
import { Loading } from "./components/exception/loading.js";
import { NotificationCard } from "./components/menu/index.js";
+import { SessionContextProvider } from "./context/session.js";
import { SettingsProvider } from "./context/settings.js";
import {
revalidateBankAccountDetails,
@@ -52,6 +53,10 @@ import {
revalidateManagedInstanceDetails,
} from "./hooks/instance.js";
import {
+ revalidateInstanceOrders,
+ revalidateOrderDetails,
+} from "./hooks/order.js";
+import {
revalidateInstanceOtpDevices,
revalidateOtpDeviceDetails,
} from "./hooks/otp.js";
@@ -63,6 +68,10 @@ import {
revalidateInstanceTemplates,
revalidateTemplateDetails,
} from "./hooks/templates.js";
+import {
+ revalidateTokenFamilies,
+ revalidateTokenFamilyDetails,
+} from "./hooks/tokenfamily.js";
import { revalidateInstanceTransfers } from "./hooks/transfer.js";
import {
revalidateInstanceWebhooks,
@@ -74,11 +83,7 @@ import {
buildDefaultBackendBaseURL,
fetchSettings,
} from "./settings.js";
-import {
- revalidateInstanceOrders,
- revalidateOrderDetails,
-} from "./hooks/order.js";
-import { SessionContextProvider } from "./context/session.js";
+import { revalidateInstanceCategories } from "./hooks/category.js";
const WITH_LOCAL_STORAGE_CACHE = false;
export function Application(): VNode {
@@ -95,6 +100,7 @@ export function Application(): VNode {
source={strings}
completeness={{
es: strings["es"].completeness,
+ uk: strings["uk"].completeness,
de: strings["de"].completeness,
}}
>
@@ -193,7 +199,9 @@ function localStorageProvider(): Map<unknown, unknown> {
function OnConfigError({
state,
}: {
- state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+ state:
+ | ConfigResultFail<TalerMerchantApi.TalerMerchantConfigResponse>
+ | undefined;
}): VNode {
const { i18n } = useTranslationContext();
if (!state) {
@@ -274,19 +282,38 @@ const swrCacheEvictor = new (class
await Promise.all([revalidateInstanceBankAccounts()]);
return;
}
+ case TalerMerchantInstanceCacheEviction.CREATE_CATEGORY: {
+ await Promise.all([revalidateInstanceCategories()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_CATEGORY: {
+ await Promise.all([revalidateInstanceCategories()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_CATEGORY: {
+ await Promise.all([revalidateInstanceCategories()]);
+ return;
+ }
case TalerMerchantInstanceCacheEviction.CREATE_PRODUCT: {
- await Promise.all([revalidateInstanceProducts()]);
+ await Promise.all([
+ revalidateInstanceProducts(),
+ revalidateInstanceCategories(),
+ ]);
return;
}
case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: {
await Promise.all([
revalidateProductDetails(),
revalidateInstanceProducts(),
+ revalidateInstanceCategories(),
]);
return;
}
case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: {
- await Promise.all([revalidateInstanceProducts()]);
+ await Promise.all([
+ revalidateInstanceProducts(),
+ revalidateInstanceCategories(),
+ ]);
return;
}
case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: {
@@ -345,23 +372,21 @@ const swrCacheEvictor = new (class
await Promise.all([revalidateInstanceOrders()]);
return;
}
- case TalerMerchantInstanceCacheEviction.LAST:
- // case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY:{
- // await Promise.all([
- // reva
- // ])
- // return
- // }
- // case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY:{
- // await Promise.all([
- // ])
- // return
- // }
- // case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY:{
- // await Promise.all([
- // ])
- // return
- // }
+ case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY: {
+ await Promise.all([revalidateTokenFamilies()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY: {
+ await Promise.all([revalidateTokenFamilyDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY: {
+ await Promise.all([revalidateTokenFamilies()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.LAST: {
+ return;
+ }
}
}
})();
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
index 665137415..de87db298 100644
--- a/packages/merchant-backoffice-ui/src/Routing.tsx
+++ b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -60,6 +60,9 @@ import TemplateQrPage from "./paths/instance/templates/qr/index.js";
import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
import TemplateUsePage from "./paths/instance/templates/use/index.js";
import TokenPage from "./paths/instance/token/index.js";
+import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js";
+import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js";
+import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js";
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
import TransferListPage from "./paths/instance/transfers/list/index.js";
import InstanceUpdatePage, {
@@ -70,9 +73,11 @@ 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 { LoginPage } from "./paths/login/index.js";
-import { NotFoundPage } from "./paths/notfound/index.js";
import { Settings } from "./paths/settings/index.js";
import { Notification } from "./utils/types.js";
+import ListCategories from "./paths/instance/categories/list/index.js";
+import CreateCategory from "./paths/instance/categories/create/index.js";
+import UpdateCategory from "./paths/instance/categories/update/index.js";
export enum InstancePaths {
error = "/error",
@@ -83,6 +88,10 @@ export enum InstancePaths {
bank_update = "/bank/:bid/update",
bank_new = "/bank/new",
+ category_list = "/category",
+ category_update = "/category/:cid/update",
+ category_new = "/category/new",
+
inventory_list = "/inventory",
inventory_update = "/inventory/:pid/update",
inventory_new = "/inventory/new",
@@ -91,10 +100,6 @@ export enum InstancePaths {
order_new = "/order/new",
order_details = "/order/:oid/details",
- reserves_list = "/reserves",
- reserves_details = "/reserves/:rid/details",
- reserves_new = "/reserves/new",
-
kyc = "/kyc",
transfers_list = "/transfers",
@@ -106,6 +111,10 @@ export enum InstancePaths {
templates_use = "/templates/:tid/use",
templates_qr = "/templates/:tid/qr",
+ token_family_list = "/tokenfamilies",
+ token_family_update = "/tokenfamilies/:slug/update",
+ token_family_new = "/tokenfamilies/new",
+
webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new",
@@ -117,9 +126,6 @@ export enum InstancePaths {
interface = "/interface",
}
-// eslint-disable-next-line @typescript-eslint/no-empty-function
-// const noop = () => { };
-
export enum AdminPaths {
list_instances = "/instances",
new_instance = "/instance/new",
@@ -139,7 +145,6 @@ export const publicPages = {
const history = createHashHistory();
export function Routing(_p: Props): VNode {
- // const { i18n } = useTranslationContext();
const { state } = useSessionContext();
type GlobalNotifState =
@@ -163,81 +168,10 @@ export function Routing(_p: Props): VNode {
accounts !== undefined &&
accounts.accounts.length < 1 &&
(AbsoluteTime.isNever(preference.hideMissingAccountUntil) ||
- AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 1);
+ AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 0);
const shouldLogin = state.status === "loggedOut";
- // function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
- // return function ServerErrorRedirectToImpl(
- // error: HttpError<TalerErrorDetail>,
- // ) {
- // 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.hint
- // : 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 />
- // </Fragment>
- // );
- // };
-
- // function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<unknown>) {
- // return function IfAdminCreateDefaultOrImpl(props?: T) {
- // if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) {
- // 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_ADMIN_USERNAME}
- // onConfirm={() => {
- // route(InstancePaths.bank_list);
- // }}
- // />
- // </Fragment>
- // );
- // }
- // if (props) {
- // return <Next {...props} />;
- // }
- // return <Next />;
- // };
- // }
-
if (shouldLogin) {
return (
<Fragment>
@@ -251,12 +185,24 @@ export function Routing(_p: Props): VNode {
return (
<Fragment>
<Menu />
- <BankAccountBanner />
- <BankAccountCreatePage
- onConfirm={() => {
- route(InstancePaths.bank_list);
- }}
- />
+ <Router history={history}>
+ <Route path={InstancePaths.interface} component={Settings} />
+ <Route
+ default
+ component={() => {
+ return (
+ <Fragment>
+ <BankAccountBanner />
+ <BankAccountCreatePage
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ </Fragment>
+ );
+ }}
+ />
+ </Router>
</Fragment>
);
}
@@ -267,21 +213,24 @@ export function Routing(_p: Props): VNode {
<KycBanner />
<NotificationCard notification={globalNotification} />
{error && (
- <NotificationCard
- notification={{
- message: "Internal error, please repot",
- type: "ERROR",
- description: (
- <pre>
- {
- (error instanceof Error
- ? error.stack
- : String(error)) as TranslatedString
- }
- </pre>
- ),
- }}
- />
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: "Internal error, please report",
+ type: "ERROR",
+
+ description: (
+ <pre>
+ {
+ (error instanceof Error
+ ? error.stack
+ : String(error)) as TranslatedString
+ }
+ </pre>
+ ),
+ }}
+ />
+ </Fragment>
)}
<Router
@@ -357,6 +306,39 @@ export function Routing(_p: Props): VNode {
}}
/>
{/**
+ * Category pages
+ */}
+ <Route
+ path={InstancePaths.category_list}
+ component={ListCategories}
+ onCreate={() => {
+ route(InstancePaths.category_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.category_update.replace(":cid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.category_update}
+ component={UpdateCategory}
+ onConfirm={() => {
+ route(InstancePaths.category_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.category_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.category_new}
+ component={CreateCategory}
+ onConfirm={() => {
+ route(InstancePaths.category_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.category_list);
+ }}
+ />
+ {/**
* Inventory pages
*/}
<Route
@@ -472,6 +454,39 @@ export function Routing(_p: Props): VNode {
route(InstancePaths.transfers_list);
}}
/>
+ {/* *
+ * Token family pages
+ */}
+ <Route
+ path={InstancePaths.token_family_list}
+ component={TokenFamilyListPage}
+ onCreate={() => {
+ route(InstancePaths.token_family_new);
+ }}
+ onSelect={(slug: string) => {
+ route(InstancePaths.token_family_update.replace(":slug", slug));
+ }}
+ />
+ <Route
+ path={InstancePaths.token_family_update}
+ component={TokenFamilyUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.token_family_new}
+ component={TokenFamilyCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.token_family_list);
+ }}
+ />
{/**
* Webhooks pages
*/}
@@ -595,13 +610,19 @@ export function Routing(_p: Props): VNode {
}}
/>
- <Route path={InstancePaths.kyc} component={ListKYCPage} />
+ <Route
+ path={InstancePaths.kyc}
+ component={ListKYCPage}
+ // onSelect={(id: string) => {
+ // route(InstancePaths.bank_update.replace(":bid", id));
+ // }}
+ />
<Route path={InstancePaths.interface} component={Settings} />
{/**
* Example pages
*/}
<Route path="/loading" component={Loading} />
- <Route default component={NotFoundPage} />
+ <Route default component={Redirect} to={InstancePaths.order_list} />
</Router>
</Fragment>
);
@@ -618,53 +639,9 @@ function AdminInstanceUpdatePage({
id,
...rest
}: { id: string } & InstanceUpdatePageProps): VNode {
- // const { i18n } = useTranslationContext();
-
return (
<Fragment>
- <InstanceAdminUpdatePage
- {...rest}
- instanceId={id}
- // onLoadError={(error: HttpError<TalerErrorDetail>) => {
- // 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.hint
- // : undefined,
- // type: "ERROR" as const,
- // };
- // return (
- // <Fragment>
- // <NotificationCard notification={notif} />
- // <LoginPage />
- // </Fragment>
- // );
- // }}
- // onUnauthorized={() => {
- // return (
- // <Fragment>
- // <NotificationCard
- // notification={{
- // message: i18n.str`Access denied`,
- // description: i18n.str`The access token provided is invalid`,
- // type: "ERROR",
- // }}
- // />
- // <LoginPage />
- // </Fragment>
- // );
- // }}
- />
+ <InstanceAdminUpdatePage {...rest} instanceId={id} />
</Fragment>
);
}
@@ -717,8 +694,10 @@ function KycBanner(): VNode {
kycStatus !== undefined &&
!(kycStatus instanceof TalerError) &&
kycStatus.type === "ok" &&
- !!kycStatus.body;
-
+ !!kycStatus.body &&
+ kycStatus.body.kyc_data.findIndex(
+ (d) => d.payto_kycauths !== undefined || d.access_token !== undefined,
+ ) !== -1;
const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1;
if (hidden || !needsToBeShown) return <Fragment />;
@@ -729,7 +708,7 @@ function KycBanner(): VNode {
<NotificationCard
notification={{
type: "WARN",
- message: "KYC verification needed",
+ message: i18n.str`KYC verification needed`,
description: (
<div>
<p>
diff --git a/packages/merchant-backoffice-ui/src/components/Amount.tsx b/packages/merchant-backoffice-ui/src/components/Amount.tsx
new file mode 100644
index 000000000..09f65473c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/Amount.tsx
@@ -0,0 +1,107 @@
+/*
+ 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 {
+ amountFractionalBase,
+ amountFractionalLength,
+ AmountJson,
+ Amounts,
+ AmountString,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+
+export function Amount({
+ value,
+ maxFracSize,
+ negative,
+ hideCurrency,
+ signType = "standard",
+ signDisplay = "auto",
+}: {
+ negative?: boolean;
+ value: AmountJson | AmountString;
+ maxFracSize?: number;
+ hideCurrency?: boolean;
+ signType?: "accounting" | "standard";
+ signDisplay?: "auto" | "always" | "never" | "exceptZero";
+}): VNode {
+ const aj = Amounts.jsonifyAmount(value);
+ const minFractional =
+ maxFracSize !== undefined && maxFracSize < 2 ? maxFracSize : 2;
+ const af = aj.fraction % amountFractionalBase;
+ let s = "";
+ if ((af && maxFracSize) || minFractional > 0) {
+ s += ".";
+ let n = af;
+ for (
+ let i = 0;
+ (maxFracSize === undefined || i < maxFracSize) &&
+ i < amountFractionalLength;
+ i++
+ ) {
+ if (!n && i >= minFractional) {
+ break;
+ }
+ s = s + Math.floor((n / amountFractionalBase) * 10).toString();
+ n = (n * 10) % amountFractionalBase;
+ }
+ }
+ const fontSize = 18;
+ const letterSpacing = 0;
+ const mult = 0.7;
+ return (
+ <span style={{ textAlign: "right", whiteSpace: "nowrap" }}>
+ <span
+ style={{
+ display: "inline-block",
+ fontFamily: "monospace",
+ fontSize,
+ }}
+ >
+ {negative ? (signType === "accounting" ? "(" : "-") : ""}
+ <span
+ style={{
+ display: "inline-block",
+ textAlign: "right",
+ fontFamily: "monospace",
+ fontSize,
+ letterSpacing,
+ }}
+ >
+ {aj.value}
+ </span>
+ <span
+ style={{
+ display: "inline-block",
+ width: !maxFracSize ? undefined : `${(maxFracSize + 1) * mult}em`,
+ textAlign: "left",
+ fontFamily: "monospace",
+ fontSize,
+ letterSpacing,
+ }}
+ >
+ {s}
+ {negative && signType === "accounting" ? ")" : ""}
+ </span>
+ </span>
+ {hideCurrency ? undefined : (
+ <Fragment>
+ &nbsp;
+ <span>{aj.currency}</span>
+ </Fragment>
+ )}
+ </span>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
index b1d1cac66..c1a1bcf2e 100644
--- a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
+++ b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
@@ -1,7 +1,6 @@
/*
-/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (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
@@ -15,115 +14,175 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { NotificationCard } from "./menu/index.js";
/**
* equivalent to ErrorLoading for merchant-backoffice which uses notification-card
- * @param param0
- * @returns
+ * @param param0
+ * @returns
*/
-export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
- const { i18n } = useTranslationContext()
+export function ErrorLoadingMerchant({
+ error,
+}: {
+ error: TalerError;
+ showDetail?: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
switch (error.errorDetail.code) {
//////////////////
// Every error that can be produce in a Http Request
//////////////////
case TalerErrorCode.GENERIC_TIMEOUT: {
if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
- const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`The request reached a timeout, check your connection.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
- }} />
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, timeoutMs },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
- const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`The request was cancelled.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
- }} />
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request was cancelled.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, timeoutMs },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
- if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
- const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`The request reached a timeout, check your connection.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
- }} />
+ if (
+ error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)
+ ) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, timeoutMs },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
- const { requestMethod, requestUrl, throttleStats } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`A lot of request were made to the same server and this action was throttled.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)
- }} />
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`A lot of request were made to the same server and this action was throttled.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, throttleStats },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
- if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
- const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`The response of the request is malformed.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)
- }} />
+ if (
+ error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)
+ ) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } =
+ error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The response of the request is malformed.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, httpStatusCode, validationError },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.WALLET_NETWORK_ERROR: {
if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
- const { requestMethod, requestUrl } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`Could not complete the request due to a network problem.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl }, undefined, 2)
- }} />
+ const { requestMethod, requestUrl } = error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Could not complete the request due to a network problem.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
- const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`Unexpected request error.`,
- description: error.message,
- details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)
- }} />
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } =
+ error.errorDetail;
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected request error.`,
+ description: error.message,
+ details: JSON.stringify(
+ { requestMethod, requestUrl, httpStatusCode, errorResponse },
+ undefined,
+ 2,
+ ),
+ }}
+ />
+ );
}
- assertUnreachable(1 as never)
+ break;
}
//////////////////
- // Every other error
+ // Every other error
//////////////////
// case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
// return <Attention type="danger" title={i18n.str``}>
@@ -133,14 +192,34 @@ export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError,
// Default message for unhandled case
//////////////////
default: {
- return <NotificationCard
- notification={{
- type: "ERROR",
- message: i18n.str`Unexpected error.`,
- description: error.message,
- details: JSON.stringify(error.errorDetail, undefined, 2)
- }} />
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected error.`,
+ description: error.message,
+ details: JSON.stringify(error.errorDetail, undefined, 2),
+ }}
+ />
+ );
}
}
+ /**
+ * This should not happen
+ *
+ * The only reason why this is possible is because the case statement
+ * follows and if `if (error.hasErrorCode` which returned false.
+ *
+ * TODO: add a better check
+ */
+ return (
+ <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected error.`,
+ description: error.message,
+ details: JSON.stringify(error.errorDetail, undefined, 2),
+ }}
+ />
+ );
}
-
diff --git a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
index 246ce0229..ad1017257 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx
@@ -25,9 +25,20 @@ export function QR({ text }: { text: string }): VNode {
qr.addData(text);
qr.make();
if (divRef.current) {
- divRef.current.innerHTML = qr.createSvgTag({
+ const image = qr.createSvgTag({
scalable: true,
});
+ const imageURL = `data:image/svg+xml,${encodeURIComponent(image)}`;
+ divRef.current.innerHTML = `<img src=${JSON.stringify(
+ imageURL,
+ )} alt=${JSON.stringify(
+ `QR Code containing the data ${text
+ .replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#039;")}`,
+ )} />`;
}
});
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
index b0b9eaefc..9abec8630 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
@@ -19,18 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { InputProps, useField } from "./useField.js";
+import { DropdownList } from "./InputSearchOnList.js";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
+ getSuggestion?: (e: any) => Promise<{ id: string; description: string }[]>;
addonBefore?: string;
toStr?: (v?: any) => string;
fromStr?: (s: string) => any;
+ unique?: boolean;
}
-const defaultToString = (f?: any): string => f || "";
+const defaultToString = (f?: any): string => (f ? String(f) : "");
const defaultFromString = (v: string): any => v as any;
export function InputArray<T>({
@@ -39,19 +42,20 @@ export function InputArray<T>({
placeholder,
tooltip,
label,
+ unique,
help,
addonBefore,
- isValid = () => true,
+ getSuggestion,
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, value, onChange, required } = useField<T>(name);
- const error = localError || formError;
-
- const array: any[] = (value ? value! : []) as any;
+ const array: T[keyof T][] = value ? value! : [];
const [currentValue, setCurrentValue] = useState("");
+ const [suggestions, setSuggestions] = useState<
+ { id: string; description: string }[]
+ >([]);
const { i18n } = useTranslationContext();
return (
@@ -83,7 +87,15 @@ export function InputArray<T>({
disabled={readonly}
name={String(name)}
value={currentValue}
- onChange={(e): void => setCurrentValue(e.currentTarget.value)}
+ onChange={async (e): Promise<void> => {
+ const v = e.currentTarget.value;
+ setCurrentValue(v);
+ if (getSuggestion) {
+ getSuggestion(v).then((ss) => {
+ setSuggestions(ss);
+ });
+ }
+ }}
/>
{required && (
<span class="icon has-text-danger is-right">
@@ -91,43 +103,57 @@ export function InputArray<T>({
</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>
+ {getSuggestion ? undefined : (
+ <p class="control">
+ <button
+ class="button is-info has-tooltip-left"
+ disabled={!currentValue}
+ onClick={(): void => {
+ const v = fromStr(currentValue);
+ if (!unique || array.indexOf(v) === -1) {
+ onChange([v, ...array] as T[keyof T]);
+ }
+ 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>}
+
+ {suggestions.length > 0 ? (
+ <div>
+ <DropdownList
+ name={currentValue}
+ list={suggestions}
+ onSelect={(p): void => {
+ if (!unique || array.indexOf(p as any) === -1) {
+ onChange([p, ...array] as T[keyof T]);
+ }
+ setCurrentValue("");
+ setSuggestions([]);
+ }}
+ withImage={false}
+ />
+ </div>
+ ) : undefined}
{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}
+ {getSuggestion ? (v as any).description : toStr(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));
+ onChange(array.filter((f) => f !== v) as T[keyof T]);
+ setCurrentValue(getSuggestion ? (v as any).description : toStr(v));
}}
/>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
index 11396b88e..ffd3bfd16 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -60,7 +60,7 @@ export function InputCurrency<T>({
expand={expand}
toStr={(v?: AmountString) => v?.split(":")[1] || ""}
fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)}
- inputExtra={{ min: 0 }}
+ inputExtra={{ min: 0, step: 0.001 }}
>
{children}
</InputWithAddon>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
index 812505f6a..36785671b 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -124,25 +124,25 @@ export function InputDate<T>({
<span
data-tooltip={
withTimestampSupport
- ? i18n.str`change value to unknown date`
- : i18n.str`change value to empty`
+ ? 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>
+ <i18n.Translate>Clear</i18n.Translate>
</button>
</span>
)}
{withTimestampSupport && (
- <span data-tooltip={i18n.str`change value to never`}>
+ <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>
+ <i18n.Translate>Never</i18n.Translate>
</button>
</span>
)}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
index ad3cb0e32..98533a1d4 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
@@ -52,14 +52,21 @@ export function InputDuration<T>({
const { error, required, value: anyValue, onChange } = useField<T>(name);
let strValue = "";
- const value: Duration = anyValue
+ const value: Duration =
+ anyValue && anyValue.d_us !== undefined
+ ? Duration.fromTalerProtocolDuration(anyValue)
+ : anyValue;
if (!value) {
strValue = "";
} else if (value.d_ms === "forever") {
- strValue = i18n.str`forever`;
+ strValue = i18n.str`Forever`;
} else {
if (value.d_ms === undefined) {
- throw Error(`assertion error: duration should have a d_ms but got '${JSON.stringify(value)}'`)
+ throw Error(
+ `assertion error: duration should have a d_ms but got '${JSON.stringify(
+ value,
+ )}'`,
+ );
}
strValue = formatDuration(
intervalToDuration({ start: 0, end: value.d_ms }),
@@ -96,7 +103,7 @@ export function InputDuration<T>({
return (
<div class="field is-horizontal">
- <div class="field-label is-normal is-flex-grow-3">
+ <div class="field-label is-normal">
<label class="label">
{label}
{tooltip && (
@@ -107,69 +114,65 @@ export function InputDuration<T>({
</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"
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={strValue}
+ placeholder={placeholder}
onClick={() => {
if (!readonly) setOpened(true);
}}
- >
- <a class="button is-static">
- <span class="icon">
- <i class="mdi mdi-clock" />
- </span>
- </a>
- </div>
+ />
+ {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>
- {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}
+ {error && <p class="help is-danger">{error}</p>}
+ <span class="has-text-grey">{help}</span>
</div>
- <span>
- {help}
- </span>
- </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>
{opened && (
<SimpleModal onCancel={() => setOpened(false)}>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
index d284b476f..b1998a457 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
@@ -77,7 +77,9 @@ export function InputImage<T>({
readonly={readonly}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
+ console.log("on change", e, f)
if (!f || f.length != 1) {
+
return onChange(undefined!);
}
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
@@ -92,7 +94,7 @@ export function InputImage<T>({
"",
),
);
- return onChange(`data:${f[0].type};base64,${b64}` as any);
+ return onChange(`data:${f[0].type};base64,${b64}` as T[keyof T]);
});
}}
/>
@@ -102,7 +104,7 @@ export function InputImage<T>({
{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>
+ <i18n.Translate>Image must be smaller than 1 MB</i18n.Translate>
</p>
)}
{!value && (
@@ -111,7 +113,12 @@ export function InputImage<T>({
</button>
)}
{value && (
- <button class="button" onClick={() => onChange(undefined!)}>
+ <button class="button" onClick={() => {
+ if (image.current) {
+ image.current.value = ""
+ }
+ onChange(undefined!);
+ }}>
<i18n.Translate>Remove</i18n.Translate>
</button>
)}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
index fcecd8932..c09b642ad 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
@@ -44,7 +44,6 @@ export function InputPayto<T>({
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/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 4ac798afe..aa46a7f8c 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,9 +18,13 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import {
+ PaytoUri,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
@@ -30,9 +34,7 @@ import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-export interface Props<T> extends InputProps<T> {
- isValid?: (e: any) => boolean;
-}
+export interface Props<T> extends InputProps<T> {}
// type Entity = PaytoUriGeneric
// https://datatracker.ietf.org/doc/html/rfc8905
@@ -66,7 +68,7 @@ function isEthereumAddress(address: string) {
return checkAddressChecksum(address);
}
-function checkAddressChecksum(address: string) {
+function checkAddressChecksum(_address: string) {
//TODO implement ethereum checksum
return true;
}
@@ -175,24 +177,10 @@ function validateIBAN_path1(
const checksum = calculate_iban_checksum(step3);
if (checksum !== 1)
- return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return i18n.str`IBAN number is invalid, 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,
@@ -202,6 +190,20 @@ export function InputPaytoForm<T>({
const { value: initialValueStr, onChange } = useField<T>(name);
const initialPayto = parsePaytoUri(initialValueStr ?? "");
+ const { i18n } = useTranslationContext();
+
+ const targets = [
+ i18n.str`Choose one...`,
+ "iban",
+ "bitcoin",
+ "ethereum",
+ "x-taler-bank",
+ ];
+ const noTargetValue = targets[0];
+ const defaultTarget: Entity = {
+ target: noTargetValue,
+ params: {},
+ };
const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
@@ -222,23 +224,22 @@ export function InputPaytoForm<T>({
if (nv !== undefined && nv.isKnown) {
if (nv.targetType === "iban" && paths.length >= 2) {
//FIXME: workaround EBIC not supported
- paths[0] = paths[1]
+ paths[0] = paths[1];
+ delete paths[1];
}
setValue({
target: nv.targetType,
params: nv.params,
path1: paths.length >= 1 ? paths[0] : undefined,
path2: paths.length >= 2 ? paths[1] : undefined,
- });
+ });
}
}, [initialValueStr]);
- const { i18n } = useTranslationContext();
-
- const errors: FormErrors<Entity> = {
- target: value.target === noTargetValue ? i18n.str`required` : undefined,
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
+ target: value.target === noTargetValue ? i18n.str`Required` : undefined,
path1: !value.path1
- ? i18n.str`required`
+ ? i18n.str`Required`
: value.target === "iban"
? validateIBAN_path1(value.path1, i18n)
: value.target === "bitcoin"
@@ -251,35 +252,36 @@ export function InputPaytoForm<T>({
path2:
value.target === "x-taler-bank"
? !value.path2
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined
: undefined,
params: undefinedIfEmpty({
"receiver-name": !value.params?.["receiver-name"]
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
}),
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const path1WithSlash =
value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1;
- const str =
+ const pto =
hasErrors || !value.target
? undefined
- : stringifyPaytoUri({
+ : ({
targetType: value.target,
targetPath: value.path2
? `${path1WithSlash}${value.path2}`
: value.path1 ?? "",
- params: value.params ?? ({} as any),
- isKnown: false,
- });
+ params: value.params ?? {},
+ isKnown: false as const,
+ } as PaytoUri);
+
+ const str = !pto ? undefined : stringifyPaytoUri(pto);
+
useEffect(() => {
- onChange(str as any);
+ onChange(str as T[keyof T]);
}, [str]);
return (
@@ -292,7 +294,7 @@ export function InputPaytoForm<T>({
>
<InputSelector<Entity>
name="target"
- label={i18n.str`Account type`}
+ label={i18n.str`Type`}
tooltip={i18n.str`Method to use for wire transfer`}
values={targets}
readonly={readonly}
@@ -426,7 +428,9 @@ export function InputPaytoForm<T>({
name="params.receiver-name"
readonly={readonly}
label={i18n.str`Owner's name`}
+ placeholder="John Doe"
tooltip={i18n.str`Legal name of the person holding the account.`}
+ help={i18n.str`It should match the bank account name.`}
/>
</Fragment>
)}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
index 9956a6427..2cc4f07c5 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -103,7 +103,7 @@ export function InputSearchOnList<T extends Entity>({
<InputWithAddon<Search>
name="name"
label={label}
- tooltip={i18n.str`enter description or id`}
+ tooltip={i18n.str`Enter description or id`}
addonAfter={
<span class="icon">
<i class="mdi mdi-magnify" />
@@ -133,7 +133,7 @@ interface DropdownListProps<T extends Entity> {
withImage: boolean;
}
-function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
+export function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
const { i18n } = useTranslationContext();
if (!name) {
/* FIXME
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
index f567f7247..5a1a87236 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
@@ -78,14 +78,16 @@ export function InputSelector<T>({
);
})}
</select>
-
- {help}
</p>
- {required && (
- <span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
- <i class="mdi mdi-alert" />
- </span>
- )}
+ <p class="help">{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>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
index 8104d1f9f..680ddcd02 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
@@ -96,7 +96,7 @@ export function InputStock<T>({
{!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`}
+ 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,
@@ -112,7 +112,7 @@ export function InputStock<T>({
) : (
<button
class="button"
- data-tooltip={i18n.str`this product has been configured without stock control`}
+ data-tooltip={i18n.str`This product has been configured without stock control`}
disabled
>
<span>
@@ -133,17 +133,11 @@ export function InputStock<T>({
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
+ ? i18n.str`Lost can't 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">
@@ -192,7 +186,7 @@ export function InputStock<T>({
side={
<button
class="button is-danger"
- data-tooltip={i18n.str`remove stock control for this product`}
+ data-tooltip={i18n.str`Remove stock control for this product`}
onClick={(): void => {
valueHandler(undefined as any);
}}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
index 4392c7659..4957d7dda 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
@@ -18,57 +18,43 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
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 { TaxSchema as schema } from "../../schemas/index.js";
-import { FormErrors, FormProvider } from "./FormProvider.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
+import { FormProvider } from "./FormProvider.js";
import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputProps, useField } from "./useField.js";
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
}
type Entity = TalerMerchantApi.Tax;
-export function InputTaxes<T>({
- name,
- readonly,
- label,
-}: Props<keyof T>): VNode {
+export function InputTaxes<T>({ name, label }: Props<keyof T>): VNode {
const { value: taxes, onChange } = useField<T>(name);
+ const { i18n } = useTranslationContext();
const [value, valueHandler] = useState<Partial<Entity>>({});
- // const [errors, setErrors] = useState<FormErrors<Entity>>({})
- let errors: FormErrors<Entity> = {};
+ const errors = undefinedIfEmpty({
+ name: !value.name ? i18n.str`Required` : undefined,
+ tax: !value.tax
+ ? i18n.str`Required`
+ : Amounts.parse(value.tax) === undefined
+ ? i18n.str`Invalid`
+ : undefined,
+ });
- 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 hasErrors = errors !== undefined;
const submit = useCallback((): void => {
onChange([value as any, ...taxes] as any);
valueHandler({});
}, [value]);
- const { i18n } = useTranslationContext();
-
//FIXME: translating plural singular
return (
<InputGroup
@@ -76,7 +62,11 @@ export function InputTaxes<T>({
label={label}
alternative={
taxes.length > 0 && (
- <p>This product has {taxes.length} applicable taxes configured.</p>
+ <p>
+ <i18n.Translate>
+ This product has {taxes.length} applicable taxes configured.
+ </i18n.Translate>
+ </p>
)
}
>
@@ -134,7 +124,7 @@ export function InputTaxes<T>({
<div class="buttons is-right mt-5">
<button
class="button is-info"
- data-tooltip={i18n.str`add tax to the tax list`}
+ data-tooltip={i18n.str`Add tax to the tax list`}
disabled={hasErrors}
onClick={submit}
>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 8c935f33b..80ec9ab98 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -18,7 +18,7 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { InputProps, useField } from "./useField.js";
interface Props<T> extends InputProps<T> {
@@ -26,11 +26,12 @@ interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
threeState?: boolean;
+ side?: ComponentChildren;
toBoolean?: (v?: any) => boolean | undefined;
fromBoolean?: (s: boolean | undefined) => any;
}
-const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultToBoolean = (f?: any): boolean | undefined => f;
const defaultFromBoolean = (v: boolean | undefined): any => v as any;
export function InputToggle<T>({
@@ -41,6 +42,7 @@ export function InputToggle<T>({
label,
help,
threeState,
+ side,
expand,
fromBoolean = defaultFromBoolean,
toBoolean = defaultToBoolean,
@@ -56,7 +58,7 @@ export function InputToggle<T>({
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label" >
+ <label class="label">
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
@@ -71,20 +73,33 @@ export function InputToggle<T>({
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
<input
type="checkbox"
- class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
+ class={"toggle-checkbox"}
checked={toBoolean(value)}
placeholder={placeholder}
+ ref={(d) => {
+ if (d) {
+ d.indeterminate =
+ !!threeState && toBoolean(value) === undefined;
+ }
+ }}
readonly={readonly}
name={String(name)}
disabled={readonly}
onChange={onCheckboxClick}
/>
- <div class={`toggle-switch ${readonly ? "disabled" : ""}`} style={{ cursor: readonly ? "default" : undefined }}></div>
+
+ <div
+ class={`toggle-switch ${readonly ? "disabled" : ""} ${
+ toBoolean(value) === undefined ? "no-dot" : ""
+ }`}
+ style={{ cursor: readonly ? "default" : undefined }}
+ ></div>
</label>
- {help}
+ <p>{help}</p>
</p>
{error && <p class="help is-danger">{error}</p>}
</div>
+ {side}
</div>
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
index b8cd4c2d2..04bcbc2be 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -69,6 +69,7 @@ export function InputWithAddon<T>({
)}
</label>
</div>
+
<div class="field-body is-flex-grow-3">
<div class="field">
<div class="field has-addons">
diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
index f5f9d5b4f..c43d28108 100644
--- a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
@@ -1,19 +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/>
+ */
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, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<boolean>, onSelect: (id: string) => void }): VNode {
- const { i18n } = useTranslationContext()
+export function JumpToElementById({
+ testIfExist,
+ onSelect,
+ placeholder,
+ description,
+}: {
+ placeholder: TranslatedString;
+ description: TranslatedString;
+ testIfExist: (id: string) => Promise<boolean>;
+ onSelect: (id: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
- const [error, setError] = useState<string | undefined>(
- undefined,
- );
+ const [error, setError] = useState<string | undefined>(undefined);
- const [id, setId] = useState<string>()
+ const [id, setId] = useState<string>();
async function check(currentId: string | undefined): Promise<void> {
if (!currentId) {
- setError(i18n.str`missing id`);
+ setError(i18n.str`Missing id`);
return;
}
try {
@@ -22,42 +45,38 @@ export function JumpToElementById({ testIfExist, onSelect, placeholder, descript
onSelect(currentId);
setError(undefined);
} else {
- setError(i18n.str`not found`);
+ setError(i18n.str`Not found`);
}
} catch {
- setError(i18n.str`not found`);
+ 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={placeholder}
- />
- {error && <p class="help is-danger">{error}</p>}
+ 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={placeholder}
+ />
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ <span class="has-tooltip-bottom" data-tooltip={description}>
+ <button class="button" onClick={() => check(id)}>
+ <span class="icon">
+ <i class="mdi mdi-arrow-right" />
+ </span>
+ </button>
+ </span>
</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/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index efcca302f..ae1bb27d1 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -28,16 +28,16 @@ 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";
-import { TextField } from "../form/TextField.js";
export function DefaultInstanceFormFields({
readonlyId,
showId,
+ showLessFields,
}: {
readonlyId?: boolean;
+ showLessFields?: boolean;
showId: boolean;
}): VNode {
const { i18n } = useTranslationContext();
@@ -60,59 +60,63 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Legal name of the business represented by this instance.`}
/>
- <Input<Entity>
- name="email"
- label={i18n.str`Email`}
- tooltip={i18n.str`Contact email`}
- />
+ {showLessFields ? undefined : (
+ <Fragment>
+ <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.`}
- />
+ <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.`}
- />
+ <InputImage<Entity>
+ name="logo"
+ label={i18n.str`Logo`}
+ tooltip={i18n.str`Logo image.`}
+ />
- <InputGroup
- name="address"
- label={i18n.str`Address`}
- tooltip={i18n.str`Physical location of the merchant.`}
- >
- <InputLocation name="address" />
- </InputGroup>
+ <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>
+ <InputGroup
+ name="jurisdiction"
+ label={i18n.str`Jurisdiction`}
+ tooltip={i18n.str`Jurisdiction for legal disputes with the merchant.`}
+ >
+ <InputLocation name="jurisdiction" />
+ </InputGroup>
- <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.`}
- />
+ <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.`}
+ />
- <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_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
- />
+ <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>
+ )}
</Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
index a6cd8014d..07fd11638 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx
@@ -32,10 +32,8 @@ type LangsNames = {
const names: LangsNames = {
es: "Español [es]",
en: "English [en]",
- fr: "Français [fr]",
de: "Deutsch [de]",
- sv: "Svenska [sv]",
- it: "Italiano [it]",
+ uk: "Українська [uk]",
};
function getLangName(s: keyof LangsNames | string) {
@@ -69,7 +67,7 @@ export function LangSelector(): VNode {
{updatingLang && (
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
- {Object.keys(messages)
+ {Object.keys(names)
.filter((l) => l !== lang)
.map((l) => (
<a
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 4a1f6a9df..90cf22d72 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -25,6 +25,7 @@ import { Fragment, VNode, h } from "preact";
import { useSessionContext } from "../../context/session.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
import { LangSelector } from "./LangSelector.js";
+import { usePreference } from "../../hooks/preference.js";
// const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -37,15 +38,19 @@ export function Sidebar({ mobile }: Props): VNode {
const { i18n } = useTranslationContext();
const { state, logOut, config } = useSessionContext();
const kycStatus = useInstanceKYCDetails();
+ const [pref] = usePreference();
const needKYC =
kycStatus !== undefined &&
!(kycStatus instanceof TalerError) &&
kycStatus.type === "ok" &&
- !!kycStatus.body;
+ !!kycStatus.body &&
+ kycStatus.body.kyc_data.findIndex(
+ (d) => d.payto_kycauths !== undefined || d.access_token !== undefined,
+ ) !== -1;
const isLoggedIn = state.status === "loggedIn";
const hasToken = isLoggedIn && state.token !== undefined;
-
+
return (
<aside
class="aside is-placed-left is-expanded"
@@ -99,6 +104,16 @@ export function Sidebar({ mobile }: Props): VNode {
</a>
</li>
<li>
+ <a href={"/category"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-label-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Categories</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
<a href={"/transfers"} class="has-icon">
<span class="icon">
<i class="mdi mdi-arrow-left-right" />
@@ -118,9 +133,28 @@ export function Sidebar({ mobile }: Props): VNode {
</span>
</a>
</li>
- {needKYC && (
+ {pref.developerMode ? (
<li>
- <a href={"/kyc"} class="has-icon">
+ <a href={"/tokenfamilies"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-key" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Token Families</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ ) : undefined}
+ {needKYC && (
+ <li class="is-warning">
+ <a
+ href={"/kyc"}
+ class="has-icon"
+ style={{
+ backgroundColor: "darkorange",
+ color: "black",
+ }}
+ >
<span class="icon">
<i class="mdi mdi-account-check" />
</span>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index a35c07ace..d330246e4 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -28,6 +28,12 @@ function getInstanceTitle(path: string, id: string): string {
switch (path) {
case InstancePaths.settings:
return `${id}: Settings`;
+ case InstancePaths.bank_new:
+ return `${id}: New bank account`;
+ case InstancePaths.bank_list:
+ return `${id}: Bank accounts`;
+ case InstancePaths.bank_update:
+ return `${id}: Update bank Account`;
case InstancePaths.order_list:
return `${id}: Orders`;
case InstancePaths.order_new:
@@ -38,10 +44,12 @@ function getInstanceTitle(path: string, id: string): string {
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.category_list:
+ return `${id}: Category`;
+ case InstancePaths.category_new:
+ return `${id}: New category`;
+ case InstancePaths.category_update:
+ return `${id}: Update category`;
case InstancePaths.transfers_list:
return `${id}: Transfers`;
case InstancePaths.transfers_new:
@@ -53,11 +61,11 @@ function getInstanceTitle(path: string, id: string): string {
case InstancePaths.webhooks_update:
return `${id}: Update webhook`;
case InstancePaths.otp_devices_list:
- return `${id}: otp devices`;
+ return `${id}: OTP devices`;
case InstancePaths.otp_devices_new:
- return `${id}: New otp devices`;
+ return `${id}: New OTP device`;
case InstancePaths.otp_devices_update:
- return `${id}: Update otp devices`;
+ return `${id}: Update OTP device`;
case InstancePaths.templates_new:
return `${id}: New template`;
case InstancePaths.templates_update:
@@ -68,6 +76,12 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Use template`;
case InstancePaths.interface:
return `${id}: Interface`;
+ case InstancePaths.token_family_list:
+ return `${id}: Token families`;
+ case InstancePaths.token_family_new:
+ return `${id}: New token family`;
+ case InstancePaths.token_family_update:
+ return `${id}: Update token family`;
default:
return "";
}
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
index 43062d13e..fc06b0aca 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -19,19 +19,24 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import {
+ AccountLetter,
+ codecForAccountLetter,
+ PaytoString,
+ PaytoUri,
+ stringifyPaytoUri,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { useSessionContext } from "../../context/session.js";
import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
import { Spinner } from "../exception/loading.js";
import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
-import { useSessionContext } from "../../context/session.js";
-import {
- AccountLetter,
- codecForAccountLetter,
- PaytoString,
-} from "@gnu-taler/taler-util";
+import { Amount } from "../Amount.js";
interface Props {
active?: boolean;
@@ -140,7 +145,13 @@ export function ContinueModal({
);
}
-export function SimpleModal({ onCancel, children }: any): VNode {
+export function SimpleModal({
+ onCancel,
+ children,
+}: {
+ onCancel: () => void;
+ children: ComponentChildren;
+}): VNode {
return (
<div class="modal is-active">
<div class="modal-background " onClick={onCancel} />
@@ -230,15 +241,13 @@ export function ImportingAccountModal({
parsed !== undefined ? codecForAccountLetter().decode(parsed) : undefined;
} catch (e) {
account = undefined;
- if (e instanceof Error) {
- parsingError = e.message;
- }
+ parsingError = e instanceof Error ? e.message : String(e);
}
const errors: FormErrors<{ letter: string }> = {
letter: !letter
- ? i18n.str`required`
+ ? i18n.str`Required`
: parsed === undefined
- ? i18n.str`letter should be a JSON string`
+ ? i18n.str`Letter must be a JSON string`
: account === undefined
? i18n.str`JSON string is invalid`
: undefined,
@@ -288,6 +297,378 @@ export function ImportingAccountModal({
);
}
+interface CompareAccountsModalProps {
+ onCancel: () => void;
+ onConfirm: (account: PaytoString) => void;
+ formPayto: PaytoUri | undefined;
+ testPayto: PaytoUri;
+}
+
+function getHostFromHostPath(s: string | undefined) {
+ if (!s) return undefined;
+ try {
+ const u = new URL(`https://${s}`);
+ const endpath = u.pathname.lastIndexOf("/");
+ return u.origin + u.pathname.substring(0, endpath);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+function getAccountIdFromHostPath(s: string | undefined) {
+ if (!s) return undefined;
+ try {
+ const u = new URL(`https://${s}`);
+ const endpath = u.pathname.lastIndexOf("/");
+ return u.pathname.substring(endpath + 1);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+export function CompareAccountsModal({
+ onCancel,
+ onConfirm,
+ formPayto,
+ testPayto,
+}: CompareAccountsModalProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <ConfirmModal
+ label={i18n.str`Correct form`}
+ description={i18n.str`Comparing account details`}
+ active
+ onCancel={onCancel}
+ onConfirm={() => onConfirm(stringifyPaytoUri(testPayto))}
+ >
+ <p>
+ <i18n.Translate>
+ Testing against the account info URL succeeded but the account
+ information reported is different with the account details form.
+ </i18n.Translate>
+ </p>
+ <div class="table-container">
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <td>
+ <i18n.Translate>Field</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>In the form</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>Reported</i18n.Translate>
+ </td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <i18n.Translate>Type</i18n.Translate>
+ </td>
+ <td>{formPayto?.targetType ?? "--"}</td>
+ <td>{testPayto.targetType}</td>
+ </tr>
+ {testPayto.targetType === "iban" && (
+ <tr>
+ <td>
+ <i18n.Translate>IBAN</i18n.Translate>
+ </td>
+ <td>{formPayto?.targetPath ?? "--"}</td>
+ <td>{testPayto.targetPath}</td>
+ </tr>
+ )}
+ {testPayto.targetType === "bitcoin" && (
+ <tr>
+ <td>
+ <i18n.Translate>Address</i18n.Translate>
+ </td>
+ <td>{formPayto?.targetPath ?? "--"}</td>
+ <td>{testPayto.targetPath}</td>
+ </tr>
+ )}
+ {testPayto.targetType === "x-taler-bank" && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Host</i18n.Translate>
+ </td>
+ <td>{getHostFromHostPath(formPayto?.targetPath) ?? "--"}</td>
+ <td>{getHostFromHostPath(testPayto.targetPath)}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Account id</i18n.Translate>
+ </td>
+ <td>
+ {getAccountIdFromHostPath(formPayto?.targetPath) ?? "--"}
+ </td>
+ <td>{getAccountIdFromHostPath(testPayto.targetPath)}</td>
+ </tr>
+ </Fragment>
+ )}
+ <tr>
+ <td>
+ <i18n.Translate>Owner's name</i18n.Translate>
+ </td>
+ <td>{formPayto?.params["receiver-name"] ?? "--"}</td>
+ <td>{testPayto.params["receiver-name"]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </ConfirmModal>
+ );
+}
+
+interface ValidateBankAccountModalProps {
+ onCancel: () => void;
+ origin: PaytoUri;
+ targets: PaytoUri[];
+}
+export function ValidBankAccount({
+ onCancel,
+ origin,
+ targets,
+}: ValidateBankAccountModalProps): VNode {
+ const { i18n } = useTranslationContext();
+ const payto = targets[0];
+ const subject = payto.params["subject"];
+
+ const accountPart = !payto.isKnown ? (
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.targetPath} />
+ </Fragment>
+ ) : payto.targetType === "x-taler-bank" ? (
+ <Fragment>
+ <Row name={i18n.str`Bank host`} value={payto.host} />
+ <Row name={i18n.str`Bank account`} value={payto.account} />
+ </Fragment>
+ ) : payto.targetType === "iban" ? (
+ <Fragment>
+ {payto.bic !== undefined ? (
+ <Row name={i18n.str`BIC`} value={payto.bic} />
+ ) : undefined}
+ <Row name={i18n.str`IBAN`} value={payto.iban} />
+ </Fragment>
+ ) : undefined;
+
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
+
+ const from = !origin.isKnown
+ ? origin.targetPath
+ : origin.targetType === "iban"
+ ? origin.iban
+ : origin.targetType === "bitcoin"
+ ? `${origin.address.substring(0, 8)}...`
+ : origin.account;
+
+ return (
+ <ConfirmModal
+ label={i18n.str`Ok`}
+ description={i18n.str`Validate bank account: ${from}`}
+ active
+ onCancel={onCancel}
+ // onConfirm={onConfirm}
+ >
+ <p style={{ paddingTop: 0 }}>
+ <i18n.Translate>
+ You need to make a bank transfer with the specified subject to
+ validate that you are the owner of the account.
+ </i18n.Translate>
+ </p>
+ <div class="table-container">
+ <table>
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in
+ your banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row name={i18n.str`Subject`} value={subject} literal />
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy and paste this IBAN and the name into the receiver fields
+ in your banking app or website
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
+ {receiver ? (
+ <Row name={i18n.str`Receiver name`} value={receiver} />
+ ) : undefined}
+
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Finish the wire transfer setting smallest amount in your
+ banking app or website.
+ </i18n.Translate>
+ </td>
+ </tr>
+ {/* <Row
+ name={i18n.str`Amount`}
+ value={
+ <Amount
+ value={payto.params["amount"] as AmountString}
+ hideCurrency
+ />
+ }
+ /> */}
+
+ <tr>
+ <td colSpan={3}>
+ {/* <WarningBox style={{ margin: 0 }}> */}
+ <b>
+ <i18n.Translate>
+ Make sure ALL data is correct, including the subject and you
+ are using your selected bank account. You can use the copy
+ buttons (<CopyIcon />) to prevent typing errors or the
+ "payto://" URI below to copy just one value.
+ </i18n.Translate>
+ </b>
+ {/* </WarningBox> */}
+ </td>
+ </tr>
+
+ <tr>
+ <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can
+ use this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
+ </td>
+ <td>
+ <CopyButton getContent={() => stringifyPaytoUri(payto)} />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </ConfirmModal>
+ );
+}
+
+function Row({
+ name,
+ value,
+ literal,
+}: {
+ name: TranslatedString;
+ value: string | VNode;
+ literal?: boolean;
+}): VNode {
+ const preRef = useRef<HTMLPreElement>(null);
+ const tdRef = useRef<HTMLTableCellElement>(null);
+
+ function getContent(): string {
+ return preRef.current?.textContent || tdRef.current?.textContent || "";
+ }
+
+ return (
+ <tr>
+ <td style={{ padding: 4, width: "1%", whiteSpace: "nowrap" }}>
+ <b>{name}</b>
+ </td>
+ {literal ? (
+ <td style={{ padding: 4 }}>
+ <pre
+ ref={preRef}
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ padding: 4,
+ }}
+ >
+ {value}
+ </pre>
+ </td>
+ ) : (
+ <td ref={tdRef} style={{ padding: 4 }}>
+ {value}
+ </td>
+ )}
+ <td style={{ padding: 4 }}>
+ <CopyButton getContent={getContent} />
+ </td>
+ </tr>
+ );
+}
+
+function CopyButton({ getContent }: { getContent: () => string }): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+ <button onClick={copyText}>
+ <CopyIcon />
+ </button>
+ );
+ }
+ return (
+ // <TooltipLeft content="Copied">
+ <button disabled>
+ <CopiedIcon />
+ </button>
+ // </TooltipLeft>
+ );
+}
+
+export const CopyIcon = (): VNode => (
+ <svg height="16" viewBox="0 0 16 16" width="16">
+ <path
+ fill-rule="evenodd"
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
+ />
+ <path
+ fill-rule="evenodd"
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
+ />
+ </svg>
+);
+
+export const CopiedIcon = (): VNode => (
+ <svg height="16" viewBox="0 0 16 16" width="16">
+ <path
+ fill-rule="evenodd"
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
+ />
+ </svg>
+);
+
interface DeleteModalProps {
element: { id: string; name: string };
onCancel: () => void;
@@ -299,6 +680,7 @@ export function DeleteModal({
onCancel,
onConfirm,
}: DeleteModalProps): VNode {
+ const { i18n } = useTranslationContext();
return (
<ConfirmModal
label={`Delete instance`}
@@ -309,17 +691,27 @@ export function DeleteModal({
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
+ <i18n.Translate>
+ 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
+ </i18n.Translate>
</p>
<p>
- This action deletes the instance private key, but preserves all
- transaction data. You can still access that data after deleting the
- instance.
+ <i18n.Translate>
+ This action deletes the instance private key, but preserves all
+ transaction data. You can still access that data after deleting the
+ instance.
+ </i18n.Translate>
</p>
<p class="warning">
- Deleting an instance <b>cannot be undone</b>.
+ <i18n.Translate>
+ Deleting an instance{" "}
+ <b>
+ <i18n.Translate>Can't be undone</i18n.Translate>
+ </b>
+ .
+ </i18n.Translate>
</p>
</ConfirmModal>
);
@@ -330,6 +722,7 @@ export function PurgeModal({
onCancel,
onConfirm,
}: DeleteModalProps): VNode {
+ const { i18n } = useTranslationContext();
return (
<ConfirmModal
label={`Purge the instance`}
@@ -340,16 +733,26 @@ export function PurgeModal({
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.
+ <i18n.Translate>
+ 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.
+ </i18n.Translate>
</p>
<p>
- The instance will disappear from your list, and you will no longer be
- able to access it&apos;s data.
+ <i18n.Translate>
+ The instance will disappear from your list, and you will no longer be
+ able to access it&apos;s data.
+ </i18n.Translate>
</p>
<p class="warning">
- Purging an instance <b>cannot be undone</b>.
+ <i18n.Translate>
+ Purging an instance{" "}
+ <b>
+ <i18n.Translate>Can't be undone</i18n.Translate>
+ </b>
+ .
+ </i18n.Translate>
</p>
</ConfirmModal>
);
@@ -378,24 +781,22 @@ export function UpdateTokenModal({
const { i18n } = useTranslationContext();
const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token;
- const errors = {
+ const errors = undefinedIfEmpty({
old_token: hasInputTheCorrectOldToken
- ? i18n.str`is not the same as the current access token`
+ ? i18n.str`Is not the same as the current access token`
: undefined,
new_token: !form.new_token
- ? i18n.str`cannot be empty`
+ ? i18n.str`Required`
: form.new_token === form.old_token
- ? i18n.str`cannot be the same as the old token`
+ ? i18n.str`Can't be the same as the old token`
: undefined,
repeat_token:
form.new_token !== form.repeat_token
- ? i18n.str`is not the same`
+ ? i18n.str`Is not the same`
: undefined,
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const { state } = useSessionContext();
@@ -416,20 +817,20 @@ export function UpdateTokenModal({
<Input<State>
name="old_token"
label={i18n.str`Old access token`}
- tooltip={i18n.str`access token currently in use`}
+ 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`}
+ 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`}
+ tooltip={i18n.str`Confirm the same access token`}
inputType="password"
/>
</FormProvider>
@@ -457,21 +858,19 @@ export function SetTokenNewInstanceModal({
});
const { i18n } = useTranslationContext();
- const errors = {
+ const errors = undefinedIfEmpty({
new_token: !form.new_token
- ? i18n.str`cannot be empty`
+ ? i18n.str`Required`
: form.new_token === form.old_token
- ? i18n.str`cannot be the same as the old access token`
+ ? i18n.str`Can't be the same as the old access token`
: undefined,
repeat_token:
form.new_token !== form.repeat_token
- ? i18n.str`is not the same`
+ ? i18n.str`Is not the same`
: undefined,
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
return (
<div class="modal is-active">
@@ -493,13 +892,13 @@ export function SetTokenNewInstanceModal({
<Input<State>
name="new_token"
label={i18n.str`New access token`}
- tooltip={i18n.str`next access token to be used`}
+ 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`}
+ tooltip={i18n.str`Confirm the same access token`}
inputType="password"
/>
</FormProvider>
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
index 52ac2a1fe..ca7ee3278 100644
--- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -21,6 +21,7 @@ import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { InputNumber } from "../form/InputNumber.js";
import { InputSearchOnList } from "../form/InputSearchOnList.js";
import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { WithId } from "../../declaration.js";
type Form = {
product: TalerMerchantApi.ProductDetail & WithId;
@@ -109,7 +110,7 @@ export function InventoryProductForm({
<InputNumber<Form>
name="quantity"
label={i18n.str`Quantity`}
- tooltip={i18n.str`how many products will be added`}
+ tooltip={i18n.str`How many products will be added`}
/>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
index a127999fc..4fd6e62ad 100644
--- a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx
@@ -13,13 +13,12 @@
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 { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useCallback, useEffect, useState } from "preact/hooks";
-import * as yup from "yup";
import { useListener } from "../../hooks/listener.js";
-import { NonInventoryProductSchema as schema } from "../../schemas/index.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
import { InputCurrency } from "../form/InputCurrency.js";
@@ -69,7 +68,7 @@ export function NonInventoryProductFrom({
<div class="buttons">
<button
class="button is-success"
- data-tooltip={i18n.str`describe and add a product that is not in the inventory list`}
+ 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>
@@ -140,38 +139,39 @@ interface NonInventoryProduct {
}
export function ProductForm({ onSubscribe, initial }: ProductProps): VNode {
+ const { i18n } = useTranslationContext();
const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({
taxes: [],
...initial,
});
- let errors: FormErrors<NonInventoryProduct> = {};
- 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 errors = undefinedIfEmpty<FormErrors<NonInventoryProduct>>({
+ quantity:
+ value.quantity === undefined
+ ? i18n.str`Required`
+ : typeof value.quantity !== "number"
+ ? i18n.str`Must be a number`
+ : value.quantity < 1
+ ? i18n.str`Must be grater than 0`
+ : undefined,
+ description: !value.description ? i18n.str`Required` : undefined,
+ unit: !value.description ? i18n.str`Required` : undefined,
+ price: !value.price
+ ? i18n.str`Required`
+ : Amounts.parse(value.price) === undefined
+ ? i18n.str`Invalid`
+ : undefined,
+ });
const submit = useCallback((): Entity | undefined => {
return value as TalerMerchantApi.Product;
}, [value]);
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
useEffect(() => {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const { i18n } = useTranslationContext();
-
return (
<div>
<FormProvider<NonInventoryProduct>
@@ -183,29 +183,29 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode {
<InputImage<NonInventoryProduct>
name="image"
label={i18n.str`Image`}
- tooltip={i18n.str`photo of the product`}
+ tooltip={i18n.str`Photo of the product.`}
/>
<Input<NonInventoryProduct>
name="description"
inputType="multiline"
label={i18n.str`Description`}
- tooltip={i18n.str`full product description`}
+ tooltip={i18n.str`Full product description.`}
/>
<Input<NonInventoryProduct>
name="unit"
label={i18n.str`Unit`}
- tooltip={i18n.str`name of the product 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`}
+ 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`}
+ tooltip={i18n.str`How many products will be added.`}
/>
<InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} />
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index dede0008f..03afd3222 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -19,17 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ AmountString,
+ Amounts,
+ TalerError,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h } from "preact";
import { useCallback, useEffect, useState } from "preact/hooks";
-import * as yup from "yup";
import { useSessionContext } from "../../context/session.js";
-import {
- ProductCreateSchema as createSchema,
- ProductUpdateSchema as updateSchema,
-} from "../../schemas/index.js";
-import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { undefinedIfEmpty } from "../../utils/table.js";
+import { FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
import { InputCurrency } from "../form/InputCurrency.js";
import { InputImage } from "../form/InputImage.js";
@@ -37,8 +38,13 @@ 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";
+import { InputArray } from "../form/InputArray.js";
+import { useInstanceCategories } from "../../hooks/category.js";
+import { ErrorLoadingMerchant } from "../ErrorLoadingMerchant.js";
-type Entity = TalerMerchantApi.ProductDetail & { product_id: string };
+type Entity = TalerMerchantApi.ProductDetail & {
+ product_id: string;
+};
interface Props {
onSubscribe: (c?: () => Entity | undefined) => void;
@@ -47,13 +53,33 @@ interface Props {
}
export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
- const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({
+ const { i18n } = useTranslationContext();
+ const { state, lib } = useSessionContext();
+ // FIXME: if the category list is big the will bring a lot of info
+ // we could find a lazy way to add up on searches
+ const categoriesResult = useInstanceCategories();
+ if (!categoriesResult) return <Loading />;
+ if (categoriesResult instanceof TalerError) {
+ return <ErrorLoadingMerchant error={categoriesResult} />;
+ }
+ const categories =
+ categoriesResult.type === "fail" ? [] : categoriesResult.body.categories;
+ const [value, valueHandler] = useState<
+ Partial<
+ Entity & {
+ stock: Stock;
+ categories_map: { id: string; description: string }[];
+ }
+ >
+ >({
address: {},
description_i18n: {},
taxes: [],
+ categories: [],
next_restock: { t_s: "never" },
price: ":0" as AmountString,
...initial,
+ minimum_age: !initial?.minimum_age ? undefined : initial?.minimum_age,
stock:
!initial || initial.total_stock === -1
? undefined
@@ -65,25 +91,48 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
nextRestock: initial.next_restock,
},
});
- let errors: FormErrors<Entity> = {};
- try {
- (alreadyExist ? updateSchema : createSchema).validateSync(value, {
- abortEarly: false,
+ useEffect(() => {
+ if (!initial || !initial?.categories) return;
+
+ const ps = initial.categories.map((catId) => {
+ return lib.instance
+ .getCategoryDetails(state.token, String(catId))
+ .then((res) => {
+ return res.type === "fail"
+ ? undefined
+ : { id: String(catId), description: res.body.name };
+ });
});
- } 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 },
- {},
- );
- }
+ Promise.all(ps).then((all) => {
+ const categories_map = all.filter(notEmpty);
+ valueHandler({ ...value, categories_map });
+ });
+ }, []);
+
+ const errors = undefinedIfEmpty({
+ product_id: !value.product_id ? i18n.str`Required` : undefined,
+ description: !value.description ? i18n.str`Required` : undefined,
+ unit: !value.unit ? i18n.str`Required` : undefined,
+ price: !value.price
+ ? i18n.str`Required`
+ : Amounts.parse(value.price) === undefined
+ ? i18n.str`Invalid amount`
+ : undefined,
+ minimum_age:
+ value.minimum_age === undefined
+ ? undefined
+ : value.minimum_age < 1
+ ? i18n.str`Must be greater than 0`
+ : undefined,
+ });
+
+ if (alreadyExist && errors) {
+ // on update, we remove some validations
+ delete errors.product_id;
}
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as Record<string, unknown>)[k] !== undefined,
- );
+
+ const hasErrors = errors !== undefined;
const submit = useCallback((): Entity | undefined => {
const stock = value.stock;
@@ -100,6 +149,8 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
value.address = stock.address;
}
delete value.stock;
+ value.categories = value.categories_map?.map((d) => parseInt(d.id, 10));
+ delete value.categories_map;
if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
delete value.minimum_age;
@@ -114,8 +165,6 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const { i18n } = useTranslationContext();
- const { state } = useSessionContext();
return (
<div>
<FormProvider<Entity>
@@ -124,54 +173,80 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
object={value}
valueHandler={valueHandler}
>
+ {/**
+ * If the user press enter on any text field it will the browser will trigger
+ * the first button that it found.
+ *
+ * In this form the InputImage will be triggered and this is unwanted.
+ *
+ * As a workaround we have this non-action button which will prevent loading/unloading
+ * the image when the enter key is pressed accidentally.
+ */}
+ <button />
{alreadyExist ? undefined : (
<InputWithAddon<Entity>
name="product_id"
addonBefore={new URL("product/", state.backendUrl.href).href}
label={i18n.str`ID`}
- tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
+ 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`}
+ 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`}
+ 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 customer`}
+ tooltip={i18n.str`Is this product restricted for customer below certain age?`}
+ help={i18n.str`Minimum age of the customer`}
/>
<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`example: kg, items or liters`}
+ tooltip={i18n.str`Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers.`}
+ help={i18n.str`Example: 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`}
+ 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)`}
+ 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`}
+ tooltip={i18n.str`Taxes included in the product price, exposed to customers.`}
+ />
+ <InputArray
+ name="categories_map"
+ label={i18n.str`Categories`}
+ getSuggestion={async () => {
+ return categories.map((cat) => {
+ return { description: cat.name, id: String(cat.category_id) };
+ });
+ }}
+ help={i18n.str`Search by category description or id`}
+ tooltip={i18n.str`Categories where this product will be listed on.`}
+ unique
/>
</FormProvider>
</div>
);
}
+
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
index 4fff66fd7..401013782 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx
@@ -34,19 +34,19 @@ export function ProductList({ list, actions = [] }: Props): VNode {
<thead>
<tr>
<th>
- <i18n.Translate>image</i18n.Translate>
+ <i18n.Translate>Image</i18n.Translate>
</th>
<th>
- <i18n.Translate>description</i18n.Translate>
+ <i18n.Translate>Description</i18n.Translate>
</th>
<th>
- <i18n.Translate>quantity</i18n.Translate>
+ <i18n.Translate>Quantity</i18n.Translate>
</th>
<th>
- <i18n.Translate>unit price</i18n.Translate>
+ <i18n.Translate>Unit price</i18n.Translate>
</th>
<th>
- <i18n.Translate>total price</i18n.Translate>
+ <i18n.Translate>Total price</i18n.Translate>
</th>
<th />
</tr>
diff --git a/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx
new file mode 100644
index 000000000..a85912efa
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx
@@ -0,0 +1,138 @@
+/*
+ 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 Christian Blättler
+ */
+
+import {
+ AbsoluteTime,
+ Duration,
+ TalerMerchantApi
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useSessionContext } from "../../context/session.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputDate } from "../form/InputDate.js";
+import { InputDuration } from "../form/InputDuration.js";
+import { InputSelector } from "../form/InputSelector.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+
+type Entity = TalerMerchantApi.TokenFamilyCreateRequest;
+
+interface Props {
+ onSubscribe: (c?: () => Entity | undefined) => void;
+ initial?: Partial<Entity>;
+ alreadyExist?: boolean;
+}
+
+export function TokenFamilyForm({ onSubscribe }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity>>({
+ slug: undefined,
+ name: undefined,
+ description: undefined,
+ description_i18n: {},
+ kind: TalerMerchantApi.TokenFamilyKind.Discount,
+ duration: Duration.toTalerProtocolDuration(Duration.getForever()),
+ valid_after: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()),
+ valid_before: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()),
+ });
+
+ const { i18n } = useTranslationContext();
+
+ const errors: FormErrors<Entity> = {
+ slug: !value.slug ? i18n.str`Required` : undefined,
+ name: !value.name ? i18n.str`Required` : undefined,
+ description: !value.description ? i18n.str`Required` : undefined,
+ valid_after: !value.valid_after ? undefined : undefined,
+ valid_before: !value.valid_before ? i18n.str`Required` : undefined,
+ duration: !value.duration ? i18n.str`Required` : undefined,
+ kind: !value.kind ? i18n.str`Required` : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submit = useCallback((): Entity | undefined => {
+ // HACK: Think about how this can be done better
+ return value as Entity;
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { state } = useSessionContext();
+
+ return (
+ <div>
+ <FormProvider<Entity>
+ name="token_family"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <InputWithAddon<Entity>
+ name="slug"
+ addonBefore={new URL("tokenfamily/", state.backendUrl.href).href}
+ label={i18n.str`Slug`}
+ tooltip={i18n.str`Token family slug to use in URLs (for internal use only)`}
+ />
+ <InputSelector<Entity>
+ name="kind"
+ label={i18n.str`Kind`}
+ tooltip={i18n.str`Token family kind`}
+ values={["discount", "subscription"]}
+ />
+ <Input<Entity>
+ name="name"
+ inputType="text"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`User-readable token family name`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Token family description for customers`}
+ />
+ <InputDate<Entity>
+ name="valid_after"
+ label={i18n.str`Valid After`}
+ tooltip={i18n.str`Token family can issue tokens after this date`}
+ withTimestampSupport
+ />
+ <InputDate<Entity>
+ name="valid_before"
+ label={i18n.str`Valid Before`}
+ tooltip={i18n.str`Token family can issue tokens until this date`}
+ withTimestampSupport
+ />
+ <InputDuration<Entity>
+ name="duration"
+ label={i18n.str`Duration`}
+ tooltip={i18n.str`Validity duration of a issued token`}
+ withForever
+ />
+ </FormProvider>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts
index fa5e14ab3..af7900645 100644
--- a/packages/merchant-backoffice-ui/src/context/session.ts
+++ b/packages/merchant-backoffice-ui/src/context/session.ts
@@ -18,6 +18,7 @@ import {
AccessToken,
Codec,
TalerMerchantApi,
+ TalerMerchantConfigResponse,
buildCodecForObject,
codecForString,
codecForURL,
@@ -99,7 +100,7 @@ export const defaultState = (url: URL): SavedSession => {
export interface SessionStateHandler {
lib: MerchantLib;
- config: TalerMerchantApi.VersionResponse;
+ config: TalerMerchantConfigResponse;
state: SessionState;
/**
@@ -122,11 +123,6 @@ export interface SessionStateHandler {
impersonate(baseUrl: URL): void;
}
-const SESSION_STATE_KEY = buildStorageKey(
- "merchant-session",
- codecForSessionState(),
-);
-
export const DEFAULT_ADMIN_USERNAME = "default";
export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
@@ -144,9 +140,9 @@ export const useSessionContext = (): SessionStateHandler => useContext(Context);
* Infer the instance name based on the URL.
* Create the instance of the merchant api http rest.
* Returns API that handle impersonation.
- *
- * @param param0
- * @returns
+ *
+ * @param param0
+ * @returns
*/
export const SessionContextProvider = ({
children,
@@ -162,8 +158,13 @@ export const SessionContextProvider = ({
} = useMerchantApiContext();
const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn");
const [currentConfig, setCurrentConfig] =
- useState<TalerMerchantApi.VersionResponse>();
- const { value: state, update } = useLocalStorage(
+ useState<TalerMerchantConfigResponse>();
+ const SESSION_STATE_KEY = buildStorageKey(
+ `merchant-session-${merchantUrl.pathname}`,
+ codecForSessionState(),
+ );
+
+ const { value: state, update } = useLocalStorage(
SESSION_STATE_KEY,
defaultState(merchantUrl),
);
@@ -171,7 +172,7 @@ export const SessionContextProvider = ({
const currentInstance = inferInstanceName(state.backendUrl);
let lib: MerchantLib;
- let config: TalerMerchantApi.VersionResponse;
+ let config: TalerMerchantConfigResponse;
const doingImpersonation = state.backendUrl.href !== merchantUrl.href;
if (doingImpersonation) {
/**
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index 1baf80ba6..dfcda220b 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -19,6 +19,78 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-interface WithId {
+export 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;
+};
diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts
index 212ef2211..e4e50ab8e 100644
--- a/packages/merchant-backoffice-ui/src/hooks/async.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/async.ts
@@ -25,7 +25,7 @@ export interface Options {
}
export interface AsyncOperationApi<T> {
- request: (...a: any) => void;
+ request: (...a: unknown[]) => void;
cancel: () => void;
data: T | undefined;
isSlow: boolean;
@@ -34,15 +34,15 @@ export interface AsyncOperationApi<T> {
}
export function useAsync<T>(
- fn?: (...args: any) => Promise<T>,
+ fn?: (...args: unknown[]) => 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 [error, setError] = useState<string>();
const [isSlow, setSlow] = useState(false);
- const request = async (...args: any) => {
+ const request = async (...args: unknown[]) => {
if (!fn) return;
setLoading(true);
@@ -54,7 +54,7 @@ export function useAsync<T>(
const result = await fn(...args);
setData(result);
} catch (error) {
- setError(error);
+ setError(error instanceof Error ? error.message : String(error));
}
setLoading(false);
setSlow(false);
diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts
index 8857ad839..0a1d63449 100644
--- a/packages/merchant-backoffice-ui/src/hooks/bank.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts
@@ -13,18 +13,18 @@
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 {
- useMerchantApiContext
-} from "@gnu-taler/web-util/browser";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ TalerHttpError,
+ TalerMerchantManagementResultByMethod,
+} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-export interface InstanceBankAccountFilter {
-}
+export interface InstanceBankAccountFilter {}
export function revalidateInstanceBankAccounts() {
return mutate(
@@ -34,13 +34,12 @@ export function revalidateInstanceBankAccounts() {
);
}
export function useInstanceBankAccounts() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>();
async function fetcher([token, _bid]: [AccessToken, string]) {
- return await instance.listBankAccounts(token, {
+ return await lib.instance.listBankAccounts(token, {
// limit: PAGINATED_LIST_REQUEST,
// offset: bid,
// order: "dec",
@@ -50,35 +49,37 @@ export function useInstanceBankAccounts() {
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listBankAccounts">,
TalerHttpError
- >([session.token, "offset", "listBankAccounts"], fetcher);
+ >([state.token, "offset", "listBankAccounts"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
if (data.type !== "ok") return data;
// return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire)
+ const filtered = data.body.accounts.filter((a) => a.active);
+ data.body.accounts = filtered;
return data;
}
export function revalidateBankAccountDetails() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getBankAccountDetails",
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "getBankAccountDetails",
undefined,
{ revalidate: true },
);
}
export function useBankAccountDetails(h_wire: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([token, wireId]: [AccessToken, string]) {
- return await instance.getBankAccountDetails(token, wireId);
+ return await lib.instance.getBankAccountDetails(token, wireId);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getBankAccountDetails">,
TalerHttpError
- >([session.token, h_wire, "getBankAccountDetails"], fetcher);
+ >([state.token, h_wire, "getBankAccountDetails"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/hooks/category.ts b/packages/merchant-backoffice-ui/src/hooks/category.ts
new file mode 100644
index 000000000..094416ae3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/category.ts
@@ -0,0 +1,78 @@
+/*
+ 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/>
+ */
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateInstanceCategories() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listCategories",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceCategories() {
+ const { state, lib } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>();
+
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await lib.instance.listCategories(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listCategories">,
+ TalerHttpError
+ >([state.token, "offset", "listCategories"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id)
+ return data;
+}
+
+export function revalidateCategoryDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCategoryDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useCategoryDetails(deviceId: string) {
+ const { state, lib } = useSessionContext();
+
+ async function fetcher([dId, token]: [string, AccessToken]) {
+ return await lib.instance.getCategoryDetails(token, dId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getCategoryDetails">,
+ TalerHttpError
+ >([deviceId, state.token, "getCategoryDetails"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index f5f8893cd..a25b33f46 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -15,31 +15,34 @@
*/
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ TalerHttpError,
+ TalerMerchantManagementResultByMethod,
+} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-
export function revalidateInstanceDetails() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails",
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails",
undefined,
{ revalidate: true },
);
}
export function useInstanceDetails() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([token]: [AccessToken]) {
- return await instance.getCurrentInstanceDetails(token);
+ return await lib.instance.getCurrentInstanceDetails(token);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">,
TalerHttpError
- >([session.token, "getCurrentInstanceDetails"], fetcher);
+ >([state.token, "getCurrentInstanceDetails"], fetcher);
if (data) return data;
if (error) return error;
@@ -48,29 +51,28 @@ export function useInstanceDetails() {
export function revalidateInstanceKYCDetails() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus",
+ (key) =>
+ Array.isArray(key) &&
+ key[key.length - 1] === "getCurrentIntanceKycStatus",
undefined,
{ revalidate: true },
);
}
export function useInstanceKYCDetails() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([token]: [AccessToken]) {
- return await instance.getCurrentIntanceKycStatus(token, {});
+ return await lib.instance.getCurrentIntanceKycStatus(token, {});
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getCurrentIntanceKycStatus">,
TalerHttpError
- >([session.token, "getCurrentIntanceKycStatus"], fetcher);
+ >([state.token, "getCurrentIntanceKycStatus"], fetcher);
if (data) return data;
if (error) return error;
return undefined;
-
-
}
export function revalidateManagedInstanceDetails() {
@@ -81,17 +83,16 @@ export function revalidateManagedInstanceDetails() {
);
}
export function useManagedInstanceDetails(instanceId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([token, instanceId]: [AccessToken, string]) {
- return await instance.getInstanceDetails(token, instanceId);
+ return await lib.instance.getInstanceDetails(token, instanceId);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getInstanceDetails">,
TalerHttpError
- >([session.token, instanceId, "getInstanceDetails"], fetcher);
+ >([state.token, instanceId, "getInstanceDetails"], fetcher);
if (data) return data;
if (error) return error;
@@ -106,17 +107,16 @@ export function revalidateBackendInstances() {
);
}
export function useBackendInstances() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([token]: [AccessToken]) {
- return await instance.listInstances(token);
+ return await lib.instance.listInstances(token);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listInstances">,
TalerHttpError
- >([session.token, "listInstances"], fetcher);
+ >([state.token, "listInstances"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts
index d0513dc40..ecdcd5f45 100644
--- a/packages/merchant-backoffice-ui/src/hooks/order.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/order.ts
@@ -32,17 +32,16 @@ export function revalidateOrderDetails() {
);
}
export function useOrderDetails(oderId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([dId, token]: [string, AccessToken]) {
- return await instance.getOrderDetails(token, dId);
+ return await lib.instance.getOrderDetails(token, dId);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getOrderDetails">,
TalerHttpError
- >([oderId, session.token, "getOrderDetails"], fetcher);
+ >([oderId, state.token, "getOrderDetails"], fetcher);
if (data) return data;
if (error) return error;
@@ -68,13 +67,12 @@ export function useInstanceOrders(
args?: InstanceOrderFilter,
updatePosition: (d: string | undefined) => void = () => { },
) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>(args?.position);
async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) {
- return await instance.listOrders(token, {
+ return await lib.instance.listOrders(token, {
limit: PAGINATED_LIST_REQUEST,
offset: o,
order: "dec",
@@ -88,7 +86,7 @@ export function useInstanceOrders(
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listOrders">,
TalerHttpError
- >([session.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher);
+ >([state.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
index 41ed89f70..d181198db 100644
--- a/packages/merchant-backoffice-ui/src/hooks/otp.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -28,13 +28,12 @@ export function revalidateInstanceOtpDevices() {
);
}
export function useInstanceOtpDevices() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>();
async function fetcher([token, _bid]: [AccessToken, string]) {
- return await instance.listOtpDevices(token, {
+ return await lib.instance.listOtpDevices(token, {
// limit: PAGINATED_LIST_REQUEST,
// offset: bid,
// order: "dec",
@@ -44,7 +43,7 @@ export function useInstanceOtpDevices() {
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listOtpDevices">,
TalerHttpError
- >([session.token, "offset", "listOtpDevices"], fetcher);
+ >([state.token, "offset", "listOtpDevices"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
@@ -62,17 +61,16 @@ export function revalidateOtpDeviceDetails() {
);
}
export function useOtpDeviceDetails(deviceId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([dId, token]: [string, AccessToken]) {
- return await instance.getOtpDeviceDetails(token, dId);
+ return await lib.instance.getOtpDeviceDetails(token, dId);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">,
TalerHttpError
- >([deviceId, session.token, "getOtpDeviceDetails"], fetcher);
+ >([deviceId, state.token, "getOtpDeviceDetails"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
index a21d2921c..705422654 100644
--- a/packages/merchant-backoffice-ui/src/hooks/preference.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -27,6 +27,8 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
export interface Preferences {
advanceOrderMode: boolean;
+ advanceInstanceMode: boolean;
+ developerMode: boolean;
hideKycUntil: AbsoluteTime;
hideMissingAccountUntil: AbsoluteTime;
dateFormat: "ymd" | "dmy" | "mdy";
@@ -34,6 +36,8 @@ export interface Preferences {
const defaultSettings: Preferences = {
advanceOrderMode: false,
+ advanceInstanceMode: false,
+ developerMode: false,
hideKycUntil: AbsoluteTime.never(),
hideMissingAccountUntil: AbsoluteTime.never(),
dateFormat: "ymd",
@@ -42,6 +46,8 @@ const defaultSettings: Preferences = {
export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
.property("advanceOrderMode", codecForBoolean())
+ .property("advanceInstanceMode", codecForBoolean())
+ .property("developerMode", codecForBoolean())
.property("hideKycUntil", codecForAbsoluteTime)
.property("hideMissingAccountUntil", codecForAbsoluteTime)
.property(
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts
index defda5552..71b5e5045 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -15,7 +15,15 @@
*/
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AccessToken, OperationOk, TalerHttpError, TalerMerchantApi, TalerMerchantManagementErrorsByMethod, TalerMerchantManagementResultByMethod, opFixedSuccess } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ OperationOk,
+ TalerHttpError,
+ TalerMerchantApi,
+ TalerMerchantManagementErrorsByMethod,
+ TalerMerchantManagementResultByMethod,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
import _useSWR, { SWRHook, mutate } from "swr";
import { useSessionContext } from "../context/session.js";
@@ -23,7 +31,10 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
-type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number };
+export type ProductWithId = TalerMerchantApi.ProductDetail & {
+ id: string;
+ serial: number;
+};
function notUndefined(c: ProductWithId | undefined): c is ProductWithId {
return c !== undefined;
}
@@ -36,15 +47,14 @@ export function revalidateInstanceProducts() {
);
}
export function useInstanceProducts() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [offset, setOffset] = useState<number | undefined>();
async function fetcher([token, bid]: [AccessToken, number]) {
- const list = await instance.listProducts(token, {
+ const list = await lib.instance.listProducts(token, {
limit: PAGINATED_LIST_REQUEST,
- offset: bid === undefined ? undefined: String(bid),
+ offset: bid === undefined ? undefined : String(bid),
order: "dec",
});
if (list.type !== "ok") {
@@ -52,7 +62,7 @@ export function useInstanceProducts() {
}
const all: Array<ProductWithId | undefined> = await Promise.all(
list.body.products.map(async (c) => {
- const r = await instance.getProductDetails(token, c.product_id);
+ const r = await lib.instance.getProductDetails(token, c.product_id);
if (r.type === "fail") {
return undefined;
}
@@ -65,16 +75,21 @@ export function useInstanceProducts() {
}
const { data, error } = useSWR<
- OperationOk<{ products: ProductWithId[] }> |
- TalerMerchantManagementErrorsByMethod<"listProducts">,
+ | OperationOk<{ products: ProductWithId[] }>
+ | TalerMerchantManagementErrorsByMethod<"listProducts">,
TalerHttpError
- >([session.token, offset, "listProductsWithId"], fetcher);
+ >([state.token, offset, "listProductsWithId"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
if (data.type !== "ok") return data;
- return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial)
+ return buildPaginatedResult(
+ data.body.products,
+ offset,
+ setOffset,
+ (d) => d.serial,
+ );
}
export function revalidateProductDetails() {
@@ -85,17 +100,16 @@ export function revalidateProductDetails() {
);
}
export function useProductDetails(productId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([pid, token]: [string, AccessToken]) {
- return await instance.getProductDetails(token, pid);
+ return await lib.instance.getProductDetails(token, pid);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getProductDetails">,
TalerHttpError
- >([productId, session.token, "getProductDetails"], fetcher);
+ >([productId, state.token, "getProductDetails"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts
index 500a94a48..e4ee04f49 100644
--- a/packages/merchant-backoffice-ui/src/hooks/templates.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -20,7 +20,6 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
import { useSessionContext } from "../context/session.js";
-import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
@@ -35,13 +34,12 @@ export function revalidateInstanceTemplates() {
);
}
export function useInstanceTemplates() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
- const [offset, setOffset] = useState<string | undefined>();
+ const [offset] = useState<string | undefined>();
async function fetcher([token, bid]: [AccessToken, string]) {
- return await instance.listTemplates(token, {
+ return await lib.instance.listTemplates(token, {
limit: PAGINATED_LIST_REQUEST,
offset: bid,
order: "dec",
@@ -51,7 +49,7 @@ export function useInstanceTemplates() {
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listTemplates">,
TalerHttpError
- >([session.token, offset, "listTemplates"], fetcher);
+ >([state.token, offset, "listTemplates"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
@@ -70,17 +68,16 @@ export function revalidateTemplateDetails() {
);
}
export function useTemplateDetails(templateId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([tid, token]: [string, AccessToken]) {
- return await instance.getTemplateDetails(token, tid);
+ return await lib.instance.getTemplateDetails(token, tid);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getTemplateDetails">,
TalerHttpError
- >([templateId, session.token, "getTemplateDetails"], fetcher);
+ >([templateId, state.token, "getTemplateDetails"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts
new file mode 100644
index 000000000..67f8023f5
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts
@@ -0,0 +1,90 @@
+/*
+ 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 { useSessionContext } from "../context/session.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantApi, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateTokenFamilies() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listTokenFamilies",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceTokenFamilies() {
+ const { state, lib } = useSessionContext();
+
+ // const [offset, setOffset] = useState<number | undefined>();
+
+ async function fetcher([token, _bid]: [AccessToken, number]) {
+ return await lib.instance.listTokenFamilies(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid === undefined ? undefined: String(bid),
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listTokenFamilies">,
+ TalerHttpError
+ >([state.token, "offset", "listTokenFamilies"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return data;
+}
+
+export function revalidateTokenFamilyDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTokenFamilyDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTokenFamilyDetails(tokenFamilySlug: string) {
+ const { state, lib } = useSessionContext();
+
+ async function fetcher([slug, token]: [string, AccessToken]) {
+ return await lib.instance.getTokenFamilyDetails(token, slug);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getTokenFamilyDetails">,
+ TalerHttpError
+ >([tokenFamilySlug, state.token, "getTokenFamilyDetails"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return data;
+}
+
+export interface TokenFamilyAPI {
+ createTokenFamily: (
+ data: TalerMerchantApi.TokenFamilyCreateRequest,
+ ) => Promise<void>;
+ updateTokenFamily: (
+ slug: string,
+ data: TalerMerchantApi.TokenFamilyUpdateRequest,
+ ) => Promise<void>;
+ deleteTokenFamily: (slug: string) => Promise<void>;
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
index 6f77369c2..02d91c496 100644
--- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
@@ -39,13 +39,12 @@ export function useInstanceTransfers(
args?: InstanceTransferFilter,
updatePosition: (id: string | undefined) => void = (() => { }),
) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>(args?.position);
async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) {
- return await instance.listWireTransfers(token, {
+ return await lib.instance.listWireTransfers(token, {
paytoURI: p,
verified: v,
limit: PAGINATED_LIST_REQUEST,
@@ -57,7 +56,7 @@ export function useInstanceTransfers(
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listWireTransfers">,
TalerHttpError
- >([session.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher);
+ >([state.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts
index 95e1c04f3..f24c4d49b 100644
--- a/packages/merchant-backoffice-ui/src/hooks/urls.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts
@@ -40,10 +40,7 @@ export const API_GET_ORDER_BY_ID = (
url: `http://backend/instances/default/private/orders/${id}`,
});
-export const API_LIST_ORDERS: Query<
- unknown,
- TalerMerchantApi.OrderHistory
-> = {
+export const API_LIST_ORDERS: Query<unknown, TalerMerchantApi.OrderHistory> = {
method: "GET",
url: "http://backend/instances/default/private/orders",
};
@@ -76,13 +73,11 @@ export const API_DELETE_ORDER = (
// TRANSFER
////////////////////
-export const API_LIST_TRANSFERS: Query<
- unknown,
- TalerMerchantApi.TransferList
-> = {
- method: "GET",
- url: "http://backend/instances/default/private/transfers",
-};
+export const API_LIST_TRANSFERS: Query<unknown, TalerMerchantApi.TransferList> =
+ {
+ method: "GET",
+ url: "http://backend/instances/default/private/transfers",
+ };
export const API_INFORM_TRANSFERS: Query<
TalerMerchantApi.TransferInformation,
@@ -155,7 +150,7 @@ export const API_GET_INSTANCE_BY_ID = (
export const API_GET_INSTANCE_KYC_BY_ID = (
id: string,
-): Query<unknown, TalerMerchantApi.AccountKycRedirects> => ({
+): Query<unknown, TalerMerchantApi.MerchantAccountKycRedirectsResponse> => ({
method: "GET",
url: `http://backend/management/instances/${id}/kyc`,
});
@@ -170,20 +165,14 @@ export const API_LIST_INSTANCES: Query<
export const API_UPDATE_INSTANCE_BY_ID = (
id: string,
-): Query<
- TalerMerchantApi.InstanceReconfigurationMessage,
- unknown
-> => ({
+): Query<TalerMerchantApi.InstanceReconfigurationMessage, unknown> => ({
method: "PATCH",
url: `http://backend/management/instances/${id}`,
});
export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
id: string,
-): Query<
- TalerMerchantApi.InstanceAuthConfigurationMessage,
- unknown
-> => ({
+): Query<TalerMerchantApi.InstanceAuthConfigurationMessage, unknown> => ({
method: "POST",
url: `http://backend/management/instances/${id}/auth`,
});
@@ -207,7 +196,7 @@ export const API_GET_CURRENT_INSTANCE: Query<
export const API_GET_CURRENT_INSTANCE_KYC: Query<
unknown,
- TalerMerchantApi.AccountKycRedirects
+ TalerMerchantApi.MerchantAccountKycRedirectsResponse
> = {
method: "GET",
url: `http://backend/instances/default/private/kyc`,
diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
index fe37162aa..3c3744b33 100644
--- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
@@ -32,13 +32,12 @@ export function revalidateInstanceWebhooks() {
);
}
export function useInstanceWebhooks() {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>();
async function fetcher([token, _bid]: [AccessToken, string]) {
- return await instance.listWebhooks(token, {
+ return await lib.instance.listWebhooks(token, {
// limit: PAGINATED_LIST_REQUEST,
// offset: bid,
// order: "dec",
@@ -48,7 +47,7 @@ export function useInstanceWebhooks() {
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"listWebhooks">,
TalerHttpError
- >([session.token, "offset", "listWebhooks"], fetcher);
+ >([state.token, "offset", "listWebhooks"], fetcher);
if (error) return error;
if (data === undefined) return undefined;
@@ -100,17 +99,16 @@ export function revalidateWebhookDetails() {
);
}
export function useWebhookDetails(webhookId: string) {
- const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const { state, lib } = useSessionContext();
async function fetcher([hookId, token]: [string, AccessToken]) {
- return await instance.getWebhookDetails(token, hookId);
+ return await lib.instance.getWebhookDetails(token, hookId);
}
const { data, error } = useSWR<
TalerMerchantManagementResultByMethod<"getWebhookDetails">,
TalerHttpError
- >([webhookId, session.token, "getWebhookDetails"], fetcher);
+ >([webhookId, state.token, "getWebhookDetails"], fetcher);
if (data) return data;
if (error) return error;
diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po
index 66d654f64..baf462da6 100644
--- a/packages/merchant-backoffice-ui/src/i18n/de.po
+++ b/packages/merchant-backoffice-ui/src/i18n/de.po
@@ -17,8 +17,8 @@ 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: 2024-05-07 14:32+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-09-26 05:33+0000\n"
+"Last-Translator: LukBru <zur@posteo.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/de/>\n"
"Language: de\n"
@@ -26,225 +26,823 @@ msgstr ""
"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.4.3\n"
+"X-Generator: Weblate 5.5.5\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
#, c-format
-msgid "Cancel"
-msgstr "Zurück"
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+"Die Anfrage hat ihr Zeitlimit erreicht, überprüfen Sie bitte Ihre "
+"Internetverbindung."
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr "Die Anfrage wurde abgebrochen."
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+"Es wurden gleichzeitig zu viele Anfragen an denselben Server gestellt, daher "
+"ist diese Aktion zurückgestellt worden."
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr "Die Antwort auf die Anfrage wird nicht richtig dargestellt."
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+"Die Anfrage konnte aufgrund eines Netzwerkproblems nicht abgeschlossen "
+"werden."
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr "Unerwarteter Fehler bei der Anfrage."
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
+msgstr "Unerwarteter Fehler."
#: src/components/modal/index.tsx:79
#, c-format
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: src/components/modal/index.tsx:87
+#, c-format
msgid "%1$s"
msgstr "%1$s"
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr "Schließen"
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr "Weiter"
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
-msgstr ""
+msgstr "Leeren"
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr "Bestätigen"
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
+#, fuzzy, c-format
+msgid "Required"
+msgstr "erforderlich"
+
+#: src/components/modal/index.tsx:248
#, c-format
-msgid "is not the same as the current access token"
+msgid "Letter must be a JSON string"
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:250
#, c-format
-msgid "cannot be empty"
-msgstr "darf nicht leer sein"
+msgid "JSON string is invalid"
+msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:255
#, c-format
-msgid "cannot be the same as the old token"
-msgstr "muss sich vom alten Token unterscheiden"
+msgid "Import"
+msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:256
#, c-format
-msgid "is not the same"
+msgid "Importing an account from the bank"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:263
#, c-format
-msgid "You are updating the access token from instance with id %1$s"
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:271
#, c-format
-msgid "Old access token"
+msgid "Account information"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:336
#, c-format
-msgid "access token currently in use"
+msgid "Correct form"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:337
#, c-format
-msgid "New access token"
+msgid "Comparing account details"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:343
#, c-format
-msgid "next access token to be used"
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:353
#, c-format
-msgid "Repeat access token"
+msgid "Field"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:356
#, c-format
-msgid "confirm the same access token"
+msgid "In the form"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:359
#, c-format
-msgid "Clearing the access token will mean public access to the instance"
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, fuzzy, c-format
+msgid "Account id"
+msgstr "Betrag"
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, fuzzy, c-format
+msgid "Is not the same as the current access token"
+msgstr "muss sich vom alten Token unterscheiden"
+
+#: src/components/modal/index.tsx:542
+#, fuzzy, c-format
+msgid "Can't be the same as the old token"
+msgstr "muss sich vom alten Token unterscheiden"
+
+#: src/components/modal/index.tsx:546
+#, c-format
+msgid "Is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:554
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:570
+#, c-format
+msgid "Old access token"
+msgstr "Altes Zugriffstoken"
+
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:577
+#, c-format
+msgid "New access token"
+msgstr "Neues Zugriffstoken"
+
+#: src/components/modal/index.tsx:578
+#, fuzzy, c-format
+msgid "Next access token to be used"
+msgstr "Neues Zugriffstoken"
+
+#: src/components/modal/index.tsx:583
+#, c-format
+msgid "Repeat access token"
+msgstr "Zugriffstoken wiederholen"
+
+#: src/components/modal/index.tsx:584
+#, fuzzy, c-format
+msgid "Confirm the same access token"
+msgstr "Zugriffstoken wiederholen"
+
+#: src/components/modal/index.tsx:589
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr ""
+
+#: src/components/modal/index.tsx:616
+#, fuzzy, c-format
+msgid "Can't be the same as the old access token"
+msgstr "muss sich vom alten Token unterscheiden"
+
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
"backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
#, c-format
-msgid "Instances"
+msgid "Your password is incorrect"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/login/index.tsx:70
+#, fuzzy, c-format
+msgid "Your instance not found"
+msgstr "IBAN-Ländercode wurde nicht gefunden"
+
+#: src/paths/login/index.tsx:89
#, c-format
-msgid "Delete"
+msgid "Login required"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/login/index.tsx:95
#, c-format
-msgid "add new instance"
+msgid "Please enter your access token for %1$s."
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
+#, c-format
+msgid "Instances"
+msgstr "Instanzen"
+
+#: src/paths/admin/list/TableActive.tsx:94
+#, c-format
+msgid "Delete"
+msgstr "Löschen"
+
+#: src/paths/admin/list/TableActive.tsx:100
+#, fuzzy, c-format
+msgid "Add new instance"
+msgstr "neue Instanz hinzufügen"
+
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Name"
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
-msgstr ""
+msgstr "Bearbeiten"
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, fuzzy, c-format
+msgid "This is not a valid host."
+msgstr "kein gültiges JSON-Format"
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Eine IBAN besteht normalerweise aus mehr als 4 Ziffern"
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Eine IBAN besteht normalerweise aus weniger als 34 Ziffern"
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr "IBAN-Ländercode wurde nicht gefunden"
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, fuzzy, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, fuzzy, c-format
+msgid "Invalid url"
+msgstr "nicht gültig"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, fuzzy, c-format
+msgid "Account:"
+msgstr "Betrag"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, fuzzy, c-format
+msgid "Do not change"
+msgstr "URL des Exchange"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr "Benutzername"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr "Passwort"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -267,57 +865,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -360,66 +1008,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
-#, c-format
-msgid "Product"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "search products by it's description or id"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:151
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "no products found with that description"
+msgid "no match found with that description or id"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
#, c-format
-msgid "Quantity"
+msgid "Search product"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
-msgid "how many products will be added"
+msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:113
+#, fuzzy, c-format
+msgid "How many products will be added"
+msgstr "Zustelladresse der Artikel"
+
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -432,54 +1075,74 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, fuzzy, c-format
+msgid "Invalid"
+msgstr "nicht gültig"
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr "Betrag"
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -487,12 +1150,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -502,7 +1165,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -512,798 +1175,865 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "amount in the current currency"
+msgid "Amount in the current currency."
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:208
+#, fuzzy, c-format
+msgid "How many products will be added."
+msgstr "Zustelladresse der Artikel"
+
#: 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/components/product/ProductList.tsx:46
+#, fuzzy, c-format
+msgid "Unit price"
+msgstr "Gesamtpreis"
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "not valid"
-msgstr ""
+msgid "Total price"
+msgstr "Gesamtpreis"
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "must be greater than 0"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
-#, c-format
-msgid "not a valid json"
-msgstr "kein gültiges JSON-Format"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:170
-#, c-format
-msgid "should be in the future"
-msgstr "sollte in der Zukunft liegen"
-
#: src/paths/instance/orders/create/CreatePage.tsx:173
-#, c-format
-msgid "refund deadline cannot be before pay deadline"
+#, fuzzy, c-format
+msgid "Refund deadline can't be before pay deadline"
msgstr "Die Rückerstattungsfrist kann nicht vor der Zahlungsfrist liegen"
#: src/paths/instance/orders/create/CreatePage.tsx:179
-#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr "Die Überweisungsfrist kann nicht vor der Rückerstattungsfrist liegen"
-#: src/paths/instance/orders/create/CreatePage.tsx:190
-#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+#: src/paths/instance/orders/create/CreatePage.tsx:188
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr "Die Überweisungsfrist kann nicht vor der Zahlungsfrist liegen"
-#: src/paths/instance/orders/create/CreatePage.tsx:197
-#, c-format
-msgid "should have a refund deadline"
+#: src/paths/instance/orders/create/CreatePage.tsx:196
+#, fuzzy, c-format
+msgid "Must have a refund deadline"
msgstr "sollte eine Rückerstattungsfrist haben"
-#: src/paths/instance/orders/create/CreatePage.tsx:202
-#, c-format
-msgid "auto refund cannot be after refund deadline"
+#: src/paths/instance/orders/create/CreatePage.tsx:201
+#, fuzzy, c-format
+msgid "Auto refund can't be after refund deadline"
msgstr ""
"Die automatische Rückerstattung kann nicht nach der Rückerstattungsfrist "
"erfolgen"
-#: src/paths/instance/orders/create/CreatePage.tsx:360
+#: src/paths/instance/orders/create/CreatePage.tsx:208
+#, fuzzy, c-format
+msgid "Must be in the future"
+msgstr "sollte in der Zukunft liegen"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:376
+#, c-format
+msgid "Simple"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:388
+#, c-format
+msgid "Advanced"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
msgid "Manage products in order"
msgstr "Artikel in der Bestellung verwalten"
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:404
+#, c-format
+msgid "%1$s products with a total price of %2$s."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr "Liste der Artikel in der Bestellung verwalten."
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, c-format
msgid "Remove this product from the order."
msgstr "Diesen Artikel aus der Bestellung entfernen."
-#: src/paths/instance/orders/create/CreatePage.tsx:415
-#, c-format
-msgid "Total price"
-msgstr "Gesamtpreis"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:417
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr "Zu zahlender Betrag"
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr "Bestellsumme"
-#: src/paths/instance/orders/create/CreatePage.tsx:437
-#, c-format
-msgid "final order price"
-msgstr ""
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Final order price"
+msgstr "Bestellsumme"
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr "Zusammenfassung"
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr "Bezeichnung der Bestellung, die den Kunden angezeigt wird"
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr "Lieferdatum"
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr "Vom Händler zugesicherte Zustellfrist."
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
-#, c-format
-msgid "address where the products will be delivered"
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, fuzzy, c-format
+msgid "Address where the products will be delivered"
msgstr "Zustelladresse der Artikel"
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)"
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr "URL der von Kunden zu besuchenden Adresse nach erfolgter Bezahlung."
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr "Taler-Zahlungsoptionen"
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:529
+#, fuzzy, c-format
+msgid "Payment time"
msgstr "Zahlungsfrist"
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
msgid ""
-"Deadline for the customer to pay for the offer before it expires. Inventory "
-"products will be reserved until this deadline."
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:486
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, c-format
-msgid "Refund deadline"
+msgid "Default"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:487
+#: src/paths/instance/orders/create/CreatePage.tsx:561
+#, fuzzy, c-format
+msgid "Refund time"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
-msgid "Time until which the order can be refunded by the merchant."
+msgid ""
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:491
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Wire transfer deadline"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:492
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, c-format
-msgid "Deadline for the exchange to make the wire transfer."
+msgid ""
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Auto-refund deadline"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Maximum deposit fee"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
-msgid "Maximum wire fee"
+msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, 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."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
-msgid "Wire fee amortization"
+msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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."
+"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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, c-format
-msgid "Create token"
+msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, c-format
-msgid "Minimum age required"
+msgid "Additional information"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, 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"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:526
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "Min age defined by the producs is %1$s"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:534
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "Additional information"
+msgid "Custom field name"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:535
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "Custom information to be included in the contract for this order."
+msgid "Disabled"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:541
+#: src/paths/instance/orders/create/CreatePage.tsx:796
+#, fuzzy, c-format
+msgid "No deadline"
+msgstr "Zahlungsfrist"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:55
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "days"
+msgid "Could not create order"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:65
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "hours"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "minutes"
+msgid "No more stock for product with id \"%1$s\"."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
-msgid "seconds"
+msgid "Orders"
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "forever"
+msgid "Create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "%1$sM"
+msgid "Load first page"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
-msgid "%1$sY"
+msgid "Date"
+msgstr "Datum"
+
+#: src/paths/instance/orders/list/Table.tsx:193
+#, c-format
+msgid "Refund"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/list/Table.tsx:202
#, c-format
-msgid "%1$sd"
+msgid "copy url"
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "%1$sh"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/components/form/InputDuration.tsx:70
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "%1$smin"
+msgid "Load next page"
msgstr ""
-#: src/components/form/InputDuration.tsx:72
+#: src/paths/instance/orders/list/Table.tsx:233
#, c-format
-msgid "%1$ssec"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:75
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "Orders"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "create order"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "load newer orders"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:383
#, c-format
-msgid "Date"
-msgstr "Datum"
+msgid "Max refundable:"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, fuzzy, c-format
+msgid "Requested by the customer"
+msgstr "Zu zahlender Betrag"
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "Refund"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "copy url"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:225
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "load older orders"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:242
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
-msgid "No orders have been found matching your query!"
+msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:288
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "duplicated"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:299
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "invalid format"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
-msgid "this value exceed the refundable amount"
+msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:346
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
-msgid "date"
+msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "amount"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "reason"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "amount to be refunded"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid "Max refundable:"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:396
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Reason"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:397
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "Choose one..."
+msgid "Payment deadline"
+msgstr "Zahlungsfrist"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:116
+#, c-format
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:399
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid "requested by the customer"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/details/DetailPage.tsx:122
+#, fuzzy, c-format
+msgid "Transfer deadline for the exchange"
+msgstr "Die Überweisungsfrist kann nicht vor der Rückerstattungsfrist liegen"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "other"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/details/DetailPage.tsx:134
+#, fuzzy, c-format
+msgid "Where the order will be delivered"
+msgstr "Zustelladresse der Artikel"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
-msgid "why this order is being refunded"
+msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
-msgid "more information to give context"
+msgid ""
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
-msgid "Contract Terms"
+msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
-msgid "total price for the transaction"
+msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "URL for this purchase"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "Max fee"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
-msgid "Max wire fee"
+msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize "
-"wire fees on average"
+msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "Created at"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:465
+#, fuzzy, c-format
+msgid "Wired"
+msgstr "erforderlich"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:470
+#, c-format
+msgid "Refunded"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:490
+#, fuzzy, c-format
+msgid "Refund order"
+msgstr "Rückerstattet"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "time when this contract was generated"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
-msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
+msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, c-format
-msgid "where the order will be delivered"
+msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "Auto-refund delay"
+msgid "Unpaid"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:659
+#, c-format
+msgid "Pay at"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:712
+#, c-format
+msgid "Order status URL"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:716
+#, c-format
+msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
-msgid "Extra info"
+msgid "Back"
+msgstr "Zurück"
+
+#: src/paths/instance/orders/details/index.tsx:88
+#, c-format
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "Order"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "claimed"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "claimed at"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "Timeline"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "Payment details"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "Order status"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "Product list"
+msgid "Only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
-msgid "paid"
+msgid ""
+"Only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
-msgid "wired"
+msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "refunded"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "refund order"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "not refundable"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "refund"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "Refunded amount"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/list/index.tsx:114
+#, fuzzy, c-format
+msgid "Order id"
+msgstr "Bestellsumme"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid "Refund taken"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Status URL"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "Refund URI"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:636
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "unpaid"
+msgid "Internal id on the system"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:654
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "pay at"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:666
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "created at"
+msgid "Verification algorithm"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:707
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Order status URL"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Payment URI"
+msgid "Device key"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid ""
-"Unknown order status. This is an error, please contact the administrator."
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "Back"
-msgstr "Zurück"
+msgid "Your device need to have exactly the same value"
+msgstr ""
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "refund created successfully"
+msgid "Generate random secret key"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "could not create the refund"
+msgid "Random"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "select date to show nearby orders"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "order id"
+msgid "Device added successfully"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Could not add device"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "remove all filters"
+msgid "OTP Devices"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/otp_devices/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Add new devices"
+msgstr "neue Instanz hinzufügen"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid "only show paid orders"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid "Paid"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:142
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "only show orders with refunds"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:145
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid "Refunded"
-msgstr "Rückerstattet"
+msgid "There is no devices yet, add more pressing the + sign"
+msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:152
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid ""
-"only show orders where customers paid, but wire payments from payment "
-"provider are still pending"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Not wired"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "clear date filter"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "Enter an order id"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "order not found"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "could not get the order to refund"
+msgid "Template id is unknown"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Loading..."
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
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"
+"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
@@ -1313,7 +2043,7 @@ msgstr ""
#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "this product has been configured without stock control"
+msgid "This product has been configured without stock control"
msgstr ""
#: src/components/form/InputStock.tsx:119
@@ -1323,1404 +2053,1523 @@ msgstr ""
#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/components/form/InputStock.tsx:169
#, c-format
msgid "Incoming"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/components/form/InputStock.tsx:170
#, c-format
msgid "Lost"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/components/form/InputStock.tsx:185
#, c-format
msgid "Current"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "remove stock control for this product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/components/form/InputStock.tsx:195
#, c-format
msgid "without stock"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/components/form/InputStock.tsx:204
#, c-format
msgid "Next restock"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "Delivery address"
+msgid "Warehouse address"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid "Add element to the list"
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/components/product/ProductForm.tsx:120
+#, fuzzy, c-format
+msgid "Invalid amount"
+msgstr "kein gültiges JSON-Format"
+
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "illustration of the product for customers"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "product description for customers"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Age restricted"
+msgid "Product description for customers."
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/components/product/ProductForm.tsx:197
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Age restriction"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/components/product/ProductForm.tsx:198
+#, c-format
+msgid "Is this product restricted for customer below certain age?"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:199
+#, fuzzy, c-format
+msgid "Minimum age of the customer"
+msgstr "Zu zahlender Betrag"
+
+#: src/components/product/ProductForm.tsx:203
+#, c-format
+msgid "Unit name"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:204
#, c-format
msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+"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
+#: src/components/product/ProductForm.tsx:205
+#, c-format
+msgid "Example: kg, items or liters"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:209
+#, c-format
+msgid "Price per unit"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:210
#, c-format
msgid ""
-"sale price for customers, including taxes, for above units of the product"
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/components/product/ProductForm.tsx:214
#, c-format
msgid "Stock"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid ""
-"product inventory for products with finite supply (for internal use only)"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Need to complete marked fields"
+msgid "Categories"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:231
+#, c-format
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/components/product/ProductForm.tsx:232
+#, fuzzy, c-format
+msgid "Categories where this product will be listed on."
+msgstr "Zustelladresse der Artikel"
+
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "could not create product"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Products"
+msgid "Could not create product"
msgstr ""
#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "add product to inventory"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Sell"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Profit"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
msgid "Sold"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "free"
+msgid "Free"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "go to product update page"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/paths/instance/products/list/Table.tsx:278
#, 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:283
+#, fuzzy, c-format
+msgid "Remove this product from the database"
+msgstr "Diesen Artikel aus der Bestellung entfernen."
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "update the product with new price"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "update product with new price"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/paths/instance/products/list/Table.tsx:373
+#, fuzzy, c-format
+msgid "Update product with new price"
+msgstr "Artikel in der Bestellung verwalten"
+
+#: src/paths/instance/products/list/Table.tsx:384
+#, fuzzy, c-format
+msgid "Confirm update"
+msgstr "Bestätigen"
+
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "add more elements to the inventory"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "new price for the product"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "the are value with errors"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "update product with new stock and price"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/paths/instance/products/list/Table.tsx:495
#, 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"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "could not update the product"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid "product delete successfully"
+msgid "Product updated successfully"
msgstr ""
#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid "could not delete the product"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "Product id:"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:149
+#, c-format
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/paths/instance/products/list/index.tsx:165
#, 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."
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/paths/instance/products/update/UpdatePage.tsx:56
#, c-format
-msgid "it should be greater than 0"
+msgid "Product id:"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/paths/instance/products/update/index.tsx:85
#, c-format
-msgid "must be a valid URL"
+msgid "Product (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "Initial balance"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:96
#, c-format
-msgid "balance prior to deposit"
+msgid "Invalid. only characters and numbers"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/paths/instance/templates/create/CreatePage.tsx:112
#, c-format
-msgid "Exchange URL"
+msgid "Must be greater that 0"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/paths/instance/templates/create/CreatePage.tsx:119
#, c-format
-msgid "URL of exchange"
+msgid "To short"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
-msgid "Next"
+msgid "Identifier"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
-msgid "Wire method"
+msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
-msgid "method to use for wire transfer"
+msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Select one wire method"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "could not create reserve"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Valid until"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
-msgid "Created balance"
+msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/templates/create/CreatePage.tsx:221
#, c-format
-msgid "Exchange balance"
+msgid "Amount is editable"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/templates/create/CreatePage.tsx:222
#, c-format
-msgid "Picked up"
+msgid "Allow the user to select the amount to pay."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/templates/create/CreatePage.tsx:229
#, c-format
-msgid "Committed"
+msgid "Currency is editable"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/templates/create/CreatePage.tsx:230
#, c-format
-msgid "Account address"
+msgid "Allow the user to change currency."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/templates/create/CreatePage.tsx:232
#, c-format
-msgid "Subject"
-msgstr "Verwendungszweck"
+msgid "Supported currencies"
+msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/templates/create/CreatePage.tsx:233
#, c-format
-msgid "Tips"
+msgid "Supported currencies: %1$s"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Minimum age"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
-msgid "Authorized"
+msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
-msgid "Expiration"
+msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, c-format
-msgid "amount of tip"
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Justification"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "reason for the tip"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "URL after tip"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "Reserves not yet funded"
+msgid "No device"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "Reserves ready"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "add new reserve"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "Expires at"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "Initial"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/templates/list/Table.tsx:66
+#, fuzzy, c-format
+msgid "Add new templates"
+msgstr "neue Instanz hinzufügen"
+
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid ""
-"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "Expected Balance"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "could not create the tip"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "should not be empty"
+msgid "Load more templates after the last one"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/templates/list/Table.tsx:214
#, c-format
-msgid "should be greater that 0"
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/templates/list/index.tsx:91
#, c-format
-msgid "can't be empty"
+msgid "Jump to template with the given template ID"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/templates/list/index.tsx:92
#, c-format
-msgid "to short"
+msgid "Template identification"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/templates/list/index.tsx:132
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/list/index.tsx:137
#, c-format
-msgid "size of the key should be 32"
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Identifier"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/list/index.tsx:160
#, c-format
-msgid "Name of the template in URLs."
+msgid "Deleting an template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/list/index.tsx:162
+#, fuzzy, c-format
+msgid "can't be undone"
+msgstr "darf nicht leer sein"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
-msgid "Describe what this template stands for"
+msgid "Print"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Fixed summary"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "Fixed price"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "If specified, this template will create order with the same price"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "Minimum age"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "Is this contract restricted to some age?"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "Payment timeout"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid ""
-"How much time has the customer to complete the payment once the order was "
-"created."
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:171
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "Verification algorithm"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "Point-of-sale key"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "Useful to validate the purchase"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/token/DetailPage.tsx:121
+#, fuzzy, c-format
+msgid "Current access token"
+msgstr "Neues Zugriffstoken"
+
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "generate random secret key"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/token/DetailPage.tsx:142
+#, fuzzy, c-format
+msgid "Clear token"
+msgstr "Leeren"
+
+#: src/paths/instance/token/DetailPage.tsx:177
+#, fuzzy, c-format
+msgid "Confirm change"
+msgstr "Bestätigen"
+
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "random"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "show secret key"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "hide secret key"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "hide"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "show"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "could not inform template"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "Amount is required"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Order summary is required"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "New order for template"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Amount of the order"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Order summary"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "could not create order from template"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/components/tokenfamily/TokenFamilyForm.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."
+msgid "Duration"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "Fixed amount"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "Default amount"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "Default summary"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "Print"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "Setup TOTP"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "Templates"
+msgid "Go to token family update page"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
+#, fuzzy, c-format
+msgid "Remove this token family from the database"
+msgstr "Diesen Artikel aus der Bestellung entfernen."
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
#, c-format
-msgid "add new templates"
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
#, c-format
-msgid "load more templates before the first one"
+msgid "Token family updated successfully"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
#, c-format
-msgid "load newer templates"
+msgid "Could not update the token family"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
#, c-format
-msgid "delete selected templates from the database"
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
#, c-format
-msgid "use template to create new order"
+msgid "Failed to delete token family"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
#, c-format
-msgid "create qr code for the template"
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
#, c-format
-msgid "load more templates after the last one"
+msgid "Deleting a token family %1$s ."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
#, c-format
-msgid "load older templates"
+msgid "Token Family: %1$s"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "Token familty updated successfully"
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "Could not update token family"
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
#, c-format
-msgid "could not delete the template"
+msgid "Check the id, does not look valid"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
#, c-format
-msgid "could not update template"
+msgid "Must have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
-msgid "should be one of '%1$s'"
+msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
-msgid "Webhook ID to use"
+msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
-msgid "Event"
+msgid "Select one account"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
-msgid "Method"
+msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
-msgid "Method used by the webhook"
+msgid ""
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
#, c-format
-msgid "URL"
+msgid "Exchange URL"
+msgstr "URL des Exchange"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "Amount credited"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
-msgid "Header"
+msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "Header template of the webhook"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Body"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "Body template by the webhook"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/paths/instance/transfers/list/Table.tsx:64
+#, fuzzy, c-format
+msgid "Add new transfer"
+msgstr "neue Instanz hinzufügen"
+
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "Webhooks"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
-msgid "add new webhooks"
+msgid "Credit"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Confirmed"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
-msgid "load newer webhooks"
+msgid "Verified"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
-msgid "Event type"
+msgid "Executed at"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
-msgid "delete selected webhook from the database"
+msgid "yes"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
-msgid "load more webhooks after the last one"
+msgid "no"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "load older webhooks"
+msgid "never"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "webhook delete successfully"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "could not delete the webhook"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
-msgid "check the id, does not look valid"
+msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/transfers/list/ListPage.tsx:76
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, c-format
-msgid "URL doesn't have the right format"
+msgid "All accounts"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "Credited bank account"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:105
#, c-format
-msgid "Select one account"
+msgid "Only show wire transfers confirmed by the merchant"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/list/ListPage.tsx:115
#, c-format
-msgid "Bank account of the merchant where the payment was received"
+msgid "Only show wire transfers claimed by the exchange"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
-msgid "Wire transfer ID"
+msgid "Unverified"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
-"characters long"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid ""
-"Base URL of the exchange that made the transfer, should have been in the "
-"wire transfer subject"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "Amount credited"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "Actual amount that was wired to the merchant's bank account"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "could not inform transfer"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Transfers"
+msgid "Doesn't match"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "add new transfer"
+msgid "Enable access control"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "load more transfers before the first one"
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "load newer transfers"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "Credit"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "Confirmed"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "Verified"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Executed at"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "yes"
+msgid "Business name"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "no"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "unknown"
+msgid "Email"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "delete selected transfer from the database"
+msgid "Contact email"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "load more transfer after the last one"
+msgid "Website URL"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "load older transfers"
+msgid "URL."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
+msgid "Logo"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "filter by account address"
+msgid "Logo image."
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "Physical location of the merchant."
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Jurisdiction"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Unverified"
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "is not valid"
+msgid "Pay transaction fee"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "is not a number"
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "must be 1 or greater"
+msgid "Default payment delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "max 7 lines"
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "change authorization configuration"
+msgid "Default wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+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/components/form/InputPaytoForm.tsx:82
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Instance id"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/instance/update/index.tsx:108
+#, fuzzy, c-format
+msgid "Failed to update instance"
+msgstr "neue Instanz hinzufügen"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
-msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
+msgid "Must be one of '%1$s'"
+msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
-msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
+msgid "Webhook ID to use"
+msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "IBAN country code not found"
-msgstr "IBAN-Ländercode wurde nicht gefunden"
+msgid "Event"
+msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
-msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
+msgid "Pay"
+msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Target type"
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "Routing"
+msgid "GET"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Routing number."
+msgid "POST"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid "Account"
+msgid "PUT"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Account number."
+msgid "PATCH"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid "Business Identifier Code."
+msgid "HEAD"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Bank Account Number."
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
#, c-format
-msgid "Unified Payment Interface."
+msgid "URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
#, c-format
-msgid "Bitcoin protocol."
+msgid "URL of the webhook where the customer will be redirected"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
-msgid "Ethereum protocol."
+msgid ""
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Interledger protocol."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Host"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Bank host."
+msgid "order's description"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
+#, fuzzy, c-format
+msgid "order's price"
+msgstr "Bestellsumme"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid "Bank account."
+msgid "order's unique identification"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Bank account owner's name."
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, c-format
-msgid "No accounts yet."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it "
-"is used to administer other instances."
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Business name"
+msgid "Http body"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Body template by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Email"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Contact email"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Website URL"
+msgid "Could not create webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "URL."
+msgid "Webhooks"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "Logo"
+msgid "Add new webhooks"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Logo image."
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "Bank account"
+msgid "Event type"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Default max deposit fee"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "Default max wire fee"
+msgid "Webhook delete successfully"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "Default wire fee amortization"
+msgid "Header"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Physical location of the merchant."
+msgid "Body"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Jurisdiction"
+msgid "Webhook updated"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "Could not update webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "Default payment delay"
+msgid "Language"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
+msgid "Set default"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Default wire transfer delay"
+msgid "Advance order creation"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/settings/index.tsx:103
#, 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."
+msgid "Shows more options in the order creation form"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/settings/index.tsx:107
#, c-format
-msgid "Instance id"
+msgid "Advance instance settings"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/settings/index.tsx:108
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Shows more options in the instance settings form"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/settings/index.tsx:113
#, c-format
-msgid "Manage access token"
+msgid "Date format"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/settings/index.tsx:131
#, c-format
-msgid "Failed to create instance"
+msgid "How the date is going to be displayed"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/settings/index.tsx:134
#, c-format
-msgid "Login required"
+msgid "Developer mode"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/settings/index.tsx:135
#, c-format
-msgid "Please enter your access token."
+msgid ""
+"Shows more options and tools which are not intended for general audience."
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/categories/list/Table.tsx:133
+#, fuzzy, c-format
+msgid "Total products"
+msgstr "Gesamtpreis"
+
+#: src/paths/instance/categories/list/Table.tsx:164
#, c-format
-msgid "Access Token"
+msgid "Delete selected category from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/categories/list/Table.tsx:199
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "There is no categories yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/categories/list/index.tsx:90
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Category delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/categories/list/index.tsx:95
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Could not delete the category"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/categories/create/CreatePage.tsx:77
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Category name"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/categories/create/index.tsx:53
#, c-format
-msgid "Access denied"
+msgid "Category added successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/categories/create/index.tsx:59
#, c-format
-msgid "The access token provided is invalid."
+msgid "Could not add category"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Id:"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Name of the category"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
#, c-format
-msgid "The access token provided is invalid"
+msgid "Products"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
#, c-format
-msgid "Hide for today"
+msgid "Search by product description or id"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
#, c-format
-msgid "Instance"
+msgid "Products that this category will list."
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/categories/update/index.tsx:93
#, c-format
-msgid "Settings"
+msgid "Could not update category"
msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/categories/update/index.tsx:95
#, c-format
-msgid "Connection"
+msgid "Category id is unknown"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/Routing.tsx:659
#, c-format
-msgid "New"
+msgid "Without this the merchant backend will refuse to create new orders."
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/Routing.tsx:669
#, c-format
-msgid "List"
+msgid "Hide for today"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/Routing.tsx:703
#, c-format
-msgid "Log out"
+msgid "KYC verification needed"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/Routing.tsx:707
#, c-format
-msgid "Check your token is valid"
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/components/menu/SideBar.tsx:157
+#, fuzzy, c-format
+msgid "Configuration"
+msgstr "Bestätigen"
+
+#: src/components/menu/SideBar.tsx:196
#, c-format
-msgid "Couldn't access the server."
+msgid "Settings"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/components/menu/SideBar.tsx:206
+#, fuzzy, c-format
+msgid "Access token"
+msgstr "Altes Zugriffstoken"
+
+#: src/components/menu/SideBar.tsx:214
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Connection"
msgstr ""
-#: src/Application.tsx:104
+#: src/components/menu/SideBar.tsx:223
#, c-format
-msgid "Server not found"
+msgid "Interface"
msgstr ""
-#: src/Application.tsx:118
+#: src/components/menu/SideBar.tsx:264
#, c-format
-msgid "Server response with an error code"
+msgid "List"
msgstr ""
-#: src/Application.tsx:120
+#: src/components/menu/SideBar.tsx:283
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "Log out"
msgstr ""
-#: src/Application.tsx:131
+#: src/paths/admin/create/index.tsx:54
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid "Failed to create instance"
msgstr ""
-#: src/Application.tsx:144
+#: src/Application.tsx:208
#, c-format
-msgid "Unexpected Error"
+msgid "checking compatibility with server..."
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/Application.tsx:217
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Contacting the server failed"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/Application.tsx:229
#, c-format
-msgid "add element to the list"
+msgid "The server version is not supported"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/Application.tsx:230
#, c-format
-msgid "add"
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2733,12 +3582,26 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
+
+#, c-format
+#~ msgid "Subject"
+#~ msgstr "Verwendungszweck"
diff --git a/packages/merchant-backoffice-ui/src/i18n/en.po b/packages/merchant-backoffice-ui/src/i18n/en.po
index d8d0bae29..9fa25de36 100644
--- a/packages/merchant-backoffice-ui/src/i18n/en.po
+++ b/packages/merchant-backoffice-ui/src/i18n/en.po
@@ -27,223 +27,815 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
#, c-format
-msgid "Cancel"
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
msgstr ""
#: src/components/modal/index.tsx:79
#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:87
+#, c-format
msgid "%1$s"
msgstr ""
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr ""
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr ""
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr ""
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
#, c-format
-msgid "is not the same as the current access token"
+msgid "Required"
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:248
#, c-format
-msgid "cannot be empty"
+msgid "Letter must be a JSON string"
msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:250
#, c-format
-msgid "cannot be the same as the old token"
+msgid "JSON string is invalid"
msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:255
#, c-format
-msgid "is not the same"
+msgid "Import"
+msgstr ""
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr ""
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr ""
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr ""
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr ""
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, c-format
+msgid "Account id"
+msgstr ""
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, c-format
+msgid "Is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:542
+#, c-format
+msgid "Can't be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:546
+#, c-format
+msgid "Is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:554
#, c-format
msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "access token currently in use"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, c-format
msgid "New access token"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:578
#, c-format
-msgid "next access token to be used"
+msgid "Next access token to be used"
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, c-format
msgid "Repeat access token"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:584
#, c-format
-msgid "confirm the same access token"
+msgid "Confirm the same access token"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, c-format
msgid "Clearing the access token will mean public access to the instance"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:616
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Can't be the same as the old access token"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
"backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, c-format
+msgid "Your instance not found"
+msgstr ""
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/paths/login/index.tsx:95
+#, c-format
+msgid "Please enter your access token for %1$s."
+msgstr ""
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/admin/list/TableActive.tsx:100
#, c-format
-msgid "add new instance"
+msgid "Add new instance"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, c-format
+msgid "This is not a valid host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, c-format
+msgid "Invalid url"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, c-format
+msgid "Account:"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, c-format
+msgid "Do not change"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -266,57 +858,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -359,66 +1001,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
-#, c-format
-msgid "Product"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "search products by it's description or id"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:151
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "no products found with that description"
+msgid "no match found with that description or id"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, c-format
+msgid "Search product"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:113
#, c-format
-msgid "how many products will be added"
+msgid "How many products will be added"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -431,54 +1068,74 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, c-format
+msgid "Invalid"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr ""
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -486,12 +1143,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -501,7 +1158,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -511,2213 +1168,2399 @@ 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"
+msgid "Amount in the current currency."
msgstr ""
-#: src/components/product/ProductList.tsx:41
+#: src/components/product/NonInventoryProductForm.tsx:208
#, c-format
-msgid "description"
+msgid "How many products will be added."
msgstr ""
-#: src/components/product/ProductList.tsx:44
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "quantity"
+msgid "Taxes"
msgstr ""
-#: src/components/product/ProductList.tsx:47
+#: src/components/product/ProductList.tsx:46
#, c-format
-msgid "unit price"
+msgid "Unit price"
msgstr ""
-#: src/components/product/ProductList.tsx:50
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "total price"
+msgid "Total price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:153
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "required"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "not valid"
+msgid "Refund deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "must be greater than 0"
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
+#: src/paths/instance/orders/create/CreatePage.tsx:188
#, c-format
-msgid "not a valid json"
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:170
+#: src/paths/instance/orders/create/CreatePage.tsx:196
#, c-format
-msgid "should be in the future"
+msgid "Must have a refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:173
+#: src/paths/instance/orders/create/CreatePage.tsx:201
#, c-format
-msgid "refund deadline cannot be before pay deadline"
+msgid "Auto refund can't be after refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:179
+#: src/paths/instance/orders/create/CreatePage.tsx:208
#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+msgid "Must be in the future"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:190
+#: src/paths/instance/orders/create/CreatePage.tsx:376
#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+msgid "Simple"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:197
+#: src/paths/instance/orders/create/CreatePage.tsx:388
#, c-format
-msgid "should have a refund deadline"
+msgid "Advanced"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:202
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
-msgid "auto refund cannot be after refund deadline"
+msgid "Manage products in order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:360
+#: src/paths/instance/orders/create/CreatePage.tsx:404
#, c-format
-msgid "Manage products in order"
+msgid "%1$s products with a total price of %2$s."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:437
+#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
-msgid "final order price"
+msgid "Final order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "address where the products will be delivered"
+msgid "Address where the products will be delivered"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, c-format
-msgid "Payment deadline"
+msgid "Payment time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, 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"
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:492
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, c-format
-msgid "Deadline for the exchange to make the wire transfer."
+msgid "Default"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:561
#, c-format
-msgid "Auto-refund deadline"
+msgid "Refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
msgid ""
-"Time until which the wallet will automatically check for refunds without "
-"user interaction."
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:502
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Maximum deposit fee"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, 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."
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:507
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Maximum wire fee"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Wire fee amortization"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid "Custom information to be included in the contract for this order."
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:541
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, c-format
-msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgid "Additional information"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:55
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
-msgid "days"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:65
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "hours"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "minutes"
+msgid "Custom field name"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "seconds"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/create/CreatePage.tsx:796
#, c-format
-msgid "forever"
+msgid "No deadline"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sM"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "%1$sY"
+msgid "Could not create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$sd"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$sh"
+msgid "No more stock for product with id \"%1$s\"."
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
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "create order"
+msgid "Create order"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
+msgid "Load first page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, c-format
msgid "copy url"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:225
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "load older orders"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:242
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "No orders have been found matching your query!"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:288
-#, c-format
-msgid "duplicated"
+msgid "Load next page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:299
+#: src/paths/instance/orders/list/Table.tsx:233
#, 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"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "amount"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "reason"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "amount to be refunded"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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
+#: src/paths/instance/orders/list/Table.tsx:391
#, c-format
-msgid "requested by the customer"
+msgid "Requested by the customer"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "other"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "why this order is being refunded"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "more information to give context"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "total price for the transaction"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "Max wire fee"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize "
-"wire fees on average"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Created at"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "time when this contract was generated"
+msgid "Payment deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:116
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:122
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, c-format
-msgid "where the order will be delivered"
+msgid "Where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "claimed"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "claimed at"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/details/DetailPage.tsx:465
#, c-format
-msgid "wired"
+msgid "Wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/details/DetailPage.tsx:490
#, c-format
-msgid "refund order"
+msgid "Refund order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "not refundable"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, c-format
msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:636
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "unpaid"
+msgid "Unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:654
+#: src/paths/instance/orders/details/DetailPage.tsx:659
#, c-format
-msgid "pay at"
+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
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/orders/details/DetailPage.tsx:716
#, c-format
msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
msgid "Back"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/paths/instance/orders/details/index.tsx:88
#, c-format
-msgid "refund created successfully"
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "could not create the refund"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "select date to show nearby orders"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "order id"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "remove all filters"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "only show paid orders"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "Paid"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:142
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "only show orders with refunds"
+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
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
msgid ""
-"only show orders where customers paid, but wire payments from payment "
+"Only show orders where customers paid, but wire payments from payment "
"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "clear date filter"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "Enter an order id"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "order not found"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "could not get the order to refund"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/orders/list/index.tsx:114
#, c-format
-msgid "Loading..."
+msgid "Order id"
msgstr ""
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid ""
-"click here to configure the stock of the product, leave it as is and the "
-"backend will not control stock"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/components/form/InputStock.tsx:109
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Manage stock"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/components/form/InputStock.tsx:115
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "this product has been configured without stock control"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/components/form/InputStock.tsx:119
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "Infinite"
+msgid "Internal id on the system"
msgstr ""
-#: src/components/form/InputStock.tsx:136
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Incoming"
+msgid "Verification algorithm"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Lost"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Current"
+msgid "Device key"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid "remove stock control for this product"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "without stock"
+msgid "Your device need to have exactly the same value"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "Next restock"
+msgid "Generate random secret key"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "Delivery address"
+msgid "Random"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "illustration of the product for customers"
+msgid "Device added successfully"
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "product description for customers"
+msgid "Could not add device"
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "Age restricted"
+msgid "OTP Devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/paths/instance/otp_devices/list/Table.tsx:62
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Add new devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid ""
-"sale price for customers, including taxes, for above units of the product"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "Stock"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid ""
-"product inventory for products with finite supply (for internal use only)"
+msgid "There is no devices yet, add more pressing the + sign"
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Need to complete marked fields"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "could not create product"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "Products"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:73
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "add product to inventory"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "Sell"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "Profit"
+msgid "Template id is unknown"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Sold"
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "free"
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "go to product update page"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "Update"
+msgid "This product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "remove this product from the database"
+msgid "Infinite"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "update the product with new price"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/components/form/InputStock.tsx:169
#, c-format
-msgid "update product with new price"
+msgid "Incoming"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/components/form/InputStock.tsx:170
#, c-format
-msgid "add more elements to the inventory"
+msgid "Lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/components/form/InputStock.tsx:185
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Current"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "new price for the product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/components/form/InputStock.tsx:195
#, c-format
-msgid "the are value with errors"
+msgid "without stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/components/form/InputStock.tsx:204
#, c-format
-msgid "update product with new stock and price"
+msgid "Next restock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "There is no products yet, add more pressing the + sign"
+msgid "Warehouse address"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:86
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product updated successfully"
+msgid "Add element to the list"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/components/product/ProductForm.tsx:120
#, c-format
-msgid "could not update the product"
+msgid "Invalid amount"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "product delete successfully"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/paths/instance/products/list/index.tsx:109
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "could not delete the product"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Product id:"
+msgid "Product description for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/components/product/ProductForm.tsx:197
#, 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."
+msgid "Age restriction"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/components/product/ProductForm.tsx:198
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Is this product restricted for customer below certain age?"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/components/product/ProductForm.tsx:199
#, c-format
-msgid "it should be greater than 0"
+msgid "Minimum age of the customer"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/components/product/ProductForm.tsx:203
#, c-format
-msgid "must be a valid URL"
+msgid "Unit name"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/components/product/ProductForm.tsx:204
#, c-format
-msgid "Initial balance"
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "balance prior to deposit"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid "Exchange URL"
+msgid "Price per unit"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/components/product/ProductForm.tsx:210
#, c-format
-msgid "URL of exchange"
+msgid ""
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/components/product/ProductForm.tsx:214
#, c-format
-msgid "Next"
+msgid "Stock"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid "Wire method"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "method to use for wire transfer"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Select one wire method"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/components/product/ProductForm.tsx:231
#, c-format
-msgid "could not create reserve"
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/components/product/ProductForm.tsx:232
#, c-format
-msgid "Valid until"
+msgid "Categories where this product will be listed on."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "Created balance"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Exchange balance"
+msgid "Could not create product"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "Picked up"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Committed"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Account address"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
-msgid "Subject"
+msgid "Sold"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "Tips"
+msgid "Free"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
-msgid "Authorized"
+msgid "Update"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/products/list/Table.tsx:283
#, c-format
-msgid "Expiration"
+msgid "Remove this product from the database"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "amount of tip"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "Justification"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/products/list/Table.tsx:373
#, c-format
-msgid "reason for the tip"
+msgid "Update product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/products/list/Table.tsx:384
#, c-format
-msgid "URL after tip"
+msgid "Confirm update"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "Reserves not yet funded"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "Reserves ready"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "add new reserve"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "Expires at"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "Initial"
+msgid "There is no products yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid ""
-"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Product updated successfully"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid "Expected Balance"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "could not create the tip"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/products/list/index.tsx:149
#, c-format
-msgid "should not be empty"
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "should be greater that 0"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "can't be empty"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:85
+#, c-format
+msgid "Product (ID: %1$s) has been updated"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "to short"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "Invalid. only characters and numbers"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:112
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Must be greater that 0"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/create/CreatePage.tsx:119
#, c-format
-msgid "size of the key should be 32"
+msgid "To short"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
msgid "Identifier"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Fixed summary"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Fixed price"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, c-format
+msgid "Amount is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
msgid "Minimum age"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, 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
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Verification algorithm"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "Point-of-sale key"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Useful to validate the purchase"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "generate random secret key"
+msgid "No device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "random"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "show secret key"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "hide secret key"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "hide"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/paths/instance/templates/list/Table.tsx:66
#, c-format
-msgid "show"
+msgid "Add new templates"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "could not inform template"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "Amount is required"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid "Order summary is required"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "New order for template"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "Amount of the order"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "Order summary"
+msgid "Load more templates after the last one"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/templates/list/Table.tsx:214
#, c-format
-msgid "could not create order from template"
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/instance/templates/list/index.tsx:91
#, 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."
+msgid "Jump to template with the given template ID"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:92
+#, c-format
+msgid "Template identification"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:132
+#, c-format
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/paths/instance/templates/list/index.tsx:137
#, c-format
-msgid "Fixed amount"
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Default amount"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/templates/list/index.tsx:160
#, c-format
-msgid "Default summary"
+msgid "Deleting an template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/templates/list/index.tsx:162
+#, c-format
+msgid "can't be undone"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
msgid "Print"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Setup TOTP"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "Templates"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "add new templates"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "load more templates before the first one"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "load newer templates"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "delete selected templates from the database"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "use template to create new order"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid "create qr code for the template"
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "load more templates after the last one"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "load older templates"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/token/DetailPage.tsx:121
#, c-format
-msgid "could not delete the template"
+msgid "Current access token"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "could not update template"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/token/DetailPage.tsx:142
#, c-format
-msgid "should be one of '%1$s'"
+msgid "Clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/token/DetailPage.tsx:177
#, c-format
-msgid "Webhook ID to use"
+msgid "Confirm change"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "Event"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "Method"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "Method used by the webhook"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "URL"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "Header"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Header template of the webhook"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "Body"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Body template by the webhook"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Webhooks"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "add new webhooks"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Duration"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "load newer webhooks"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "Event type"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "delete selected webhook from the database"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "load older webhooks"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "Go to token family update page"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
#, c-format
-msgid "webhook delete successfully"
+msgid "Remove this token family from the database"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
#, c-format
-msgid "could not delete the webhook"
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
#, c-format
-msgid "check the id, does not look valid"
+msgid "Token family updated successfully"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Could not update the token family"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, c-format
+msgid "Failed to delete token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
+#, c-format
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, c-format
+msgid "Token familty updated successfully"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
+#, c-format
+msgid "Could not update token family"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
+#, c-format
+msgid "Check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
+#, c-format
+msgid "Must have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
#, 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
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
msgid "Amount credited"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "could not inform transfer"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Transfers"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "add new transfer"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/transfers/list/Table.tsx:64
#, c-format
-msgid "load more transfers before the first one"
+msgid "Add new transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "load newer transfers"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "unknown"
+msgid "never"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "delete selected transfer from the database"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "load more transfer after the last one"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "load older transfers"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/paths/instance/transfers/list/ListPage.tsx:76
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, c-format
-msgid "filter by account address"
+msgid "All accounts"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/paths/instance/transfers/list/ListPage.tsx:105
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Only show wire transfers confirmed by the merchant"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/paths/instance/transfers/list/ListPage.tsx:115
+#, c-format
+msgid "Only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
msgid "Unverified"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid "is not valid"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid "is not a number"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "must be 1 or greater"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "max 7 lines"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "change authorization configuration"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+msgid "Doesn't match"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:82
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Enable access control"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "IBAN country code not found"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Target type"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Business name"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "Routing"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "Routing number."
+msgid "Email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "Account"
+msgid "Contact email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "Account number."
+msgid "Website URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "Business Identifier Code."
+msgid "URL."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "Bank Account Number."
+msgid "Logo"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "Unified Payment Interface."
+msgid "Logo image."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "Bitcoin protocol."
+msgid "Physical location of the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "Ethereum protocol."
+msgid "Jurisdiction"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Interledger protocol."
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "Host"
+msgid "Pay transaction fee"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "Bank host."
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Bank account."
+msgid "Default payment delay"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "Bank account owner's name."
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "No accounts yet."
+msgid "Default wire transfer delay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it "
-"is used to administer other instances."
+"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/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "Business name"
+msgid "Instance id"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/update/index.tsx:108
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Failed to update instance"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "Email"
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "Contact email"
+msgid "Must be one of '%1$s'"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "Website URL"
+msgid "Webhook ID to use"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "URL."
+msgid "Event"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "Logo"
+msgid "Pay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Logo image."
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Bank account"
+msgid "Method"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "GET"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Default max deposit fee"
+msgid "POST"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "PUT"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Default max wire fee"
+msgid "PATCH"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
+msgid "HEAD"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Default wire fee amortization"
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Physical location of the merchant."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Jurisdiction"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "order's description"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
#, c-format
-msgid "Default payment delay"
+msgid "order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
+msgid "order's unique identification"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Default wire transfer delay"
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, 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."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "Instance id"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Http body"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Manage access token"
+msgid "Body template by the webhook"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Failed to create instance"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Login required"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Please enter your access token."
+msgid "Could not create webhook"
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "Access Token"
+msgid "Webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "Add new webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Event type"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Access denied"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid "The access token provided is invalid."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Webhook delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "The access token provided is invalid"
+msgid "Header"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid "Hide for today"
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Instance"
+msgid "Body"
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Settings"
+msgid "Webhook updated"
msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Connection"
+msgid "Could not update webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "New"
+msgid "Language"
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "List"
+msgid "Set default"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Log out"
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, c-format
+msgid "Date format"
+msgstr ""
+
+#: src/paths/settings/index.tsx:131
+#, c-format
+msgid "How the date is going to be displayed"
+msgstr ""
+
+#: src/paths/settings/index.tsx:134
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/paths/settings/index.tsx:135
+#, c-format
+msgid ""
+"Shows more options and tools which are not intended for general audience."
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:133
+#, c-format
+msgid "Total products"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, c-format
+msgid "Delete selected category from the database"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, c-format
+msgid "Category delete successfully"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:95
+#, c-format
+msgid "Could not delete the category"
+msgstr ""
+
+#: src/paths/instance/categories/create/CreatePage.tsx:77
+#, c-format
+msgid "Category name"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:53
+#, c-format
+msgid "Category added successfully"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:59
+#, c-format
+msgid "Could not add category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
+#, c-format
+msgid "Id:"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, c-format
+msgid "Name of the category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, c-format
+msgid "Search by product description or id"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
+#, c-format
+msgid "Products that this category will list."
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:93
+#, c-format
+msgid "Could not update category"
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:95
+#, c-format
+msgid "Category id is unknown"
+msgstr ""
+
+#: src/Routing.tsx:659
+#, c-format
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/Routing.tsx:703
#, c-format
-msgid "Check your token is valid"
+msgid "KYC verification needed"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/Routing.tsx:707
#, c-format
-msgid "Couldn't access the server."
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/components/menu/SideBar.tsx:157
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Configuration"
msgstr ""
-#: src/Application.tsx:104
+#: src/components/menu/SideBar.tsx:196
#, c-format
-msgid "Server not found"
+msgid "Settings"
msgstr ""
-#: src/Application.tsx:118
+#: src/components/menu/SideBar.tsx:206
#, c-format
-msgid "Server response with an error code"
+msgid "Access token"
msgstr ""
-#: src/Application.tsx:120
+#: src/components/menu/SideBar.tsx:214
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "Connection"
msgstr ""
-#: src/Application.tsx:131
+#: src/components/menu/SideBar.tsx:223
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid "Interface"
msgstr ""
-#: src/Application.tsx:144
+#: src/components/menu/SideBar.tsx:264
#, c-format
-msgid "Unexpected Error"
+msgid "List"
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/components/menu/SideBar.tsx:283
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Log out"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/paths/admin/create/index.tsx:54
#, c-format
-msgid "add element to the list"
+msgid "Failed to create instance"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/Application.tsx:208
#, c-format
-msgid "add"
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, c-format
+msgid "Contacting the server failed"
+msgstr ""
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2730,12 +3573,22 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po
index 2c4bc64a7..60b2727df 100644
--- a/packages/merchant-backoffice-ui/src/i18n/es.po
+++ b/packages/merchant-backoffice-ui/src/i18n/es.po
@@ -17,8 +17,8 @@ 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: 2024-02-13 14:40+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-09-16 19:13+0000\n"
+"Last-Translator: Sebastian Marchano <sebasjm@gmail.com>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/es/>\n"
"Language: es\n"
@@ -26,109 +26,288 @@ msgstr ""
"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"
+"X-Generator: Weblate 5.5.5\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
+#, c-format
+msgid "The request reached a timeout, check your connection."
+msgstr "El pedido a terminado en tiempo agotado, verifique su conexión."
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr "El pedido a sido cancelado."
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+"Se hicieron muchos pedidos al mismo servidor y esta acción a sido limitada."
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr "La respuesta del pedido no esta bien formada."
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr "No se pudo completar el pedido por problemas de red."
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr "Error inesperado en el pedido."
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
+msgstr "Error inesperado."
+
+#: src/components/modal/index.tsx:79
#, c-format
msgid "Cancel"
msgstr "Cancelar"
-#: src/components/modal/index.tsx:79
+#: src/components/modal/index.tsx:87
#, c-format
msgid "%1$s"
msgstr "%1$s"
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr "Cerrar"
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr "Continuar"
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr "Limpiar"
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr "Confirmar"
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
#, c-format
-msgid "is not the same as the current access token"
-msgstr "no es el mismo que el token de acceso actual"
+msgid "Required"
+msgstr "Requerido"
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:248
#, c-format
-msgid "cannot be empty"
-msgstr "no puede ser vacío"
+msgid "Letter must be a JSON string"
+msgstr "Letter debe ser una cadena JSON"
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:250
#, c-format
-msgid "cannot be the same as the old token"
-msgstr "no puede ser igual al viejo token"
+msgid "JSON string is invalid"
+msgstr "La cadena JSON es invalida"
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:255
#, c-format
-msgid "is not the same"
+msgid "Import"
+msgstr "Importar"
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr "Importando una cuenta desde el banco"
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+"Puedes exportar una configuracion de cuenta desde el perfil de cuenta del "
+"banco Libeufin. Pega el contenido en el próximo campo."
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr "Información de la cuenta"
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr "Formulario correcto"
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr "Comparando detalle de cuentas"
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+"Verificando contra la URL de información de cuenta exitoso pero la "
+"información de la cuenta reportada es diferente a los detalles de cuenta en "
+"el formulario."
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr "Campo"
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr "En el formulario"
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr "Reportado"
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr "Tipo"
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr "Dirección"
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr "Host"
+
+#: src/components/modal/index.tsx:400
+#, c-format
+msgid "Account id"
+msgstr "Identificacion de cuenta"
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr "Nombre del dueno"
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+"Si eliminas la instancia con nombre %1$s (ID: %2$s), el comerciante no podrá "
+"procesar ordenes o rembolsos"
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+"Esta accion elimina la clave privada de la instancia pero preserva toda la "
+"información transaccional. Podrás acceder a esa información después de "
+"borrar la instancia."
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, fuzzy, 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:542
+#, fuzzy, c-format
+msgid "Can't be the same as the old token"
+msgstr "no puede ser igual al viejo token"
+
+#: src/components/modal/index.tsx:546
+#, fuzzy, c-format
+msgid "Is not the same"
msgstr "no son iguales"
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:554
#, 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
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
-msgstr "Viejo token de acceso"
+msgstr "Token de acceso viejo"
-#: src/components/modal/index.tsx:332
-#, c-format
-msgid "access token currently in use"
+#: src/components/modal/index.tsx:571
+#, fuzzy, c-format
+msgid "Access token currently in use"
msgstr "acceder al token en uso actualmente"
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, 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"
+#: src/components/modal/index.tsx:578
+#, fuzzy, c-format
+msgid "Next access token to be used"
msgstr "siguiente token de acceso a usar"
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, 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"
+#: src/components/modal/index.tsx:584
+#, fuzzy, c-format
+msgid "Confirm the same access token"
msgstr "confirmar el mismo token de acceso"
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, 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"
+#: src/components/modal/index.tsx:616
+#, fuzzy, c-format
+msgid "Can't be the same as the old access token"
msgstr "no puede ser igual al anterior token de acceso"
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, 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
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
@@ -137,116 +316,542 @@ 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
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr "Establecer autorización externa"
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr "Establecer token de acceso"
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr "Operación en progreso..."
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, 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
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, fuzzy, c-format
+msgid "Your instance not found"
+msgstr "Orden no encontrada"
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr "Login necesario"
+
+#: src/paths/login/index.tsx:95
+#, fuzzy, c-format
+msgid "Please enter your access token for %1$s."
+msgstr "Por favor, introduzca su clave de acceso."
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr "Token de acceso"
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr "Instancias"
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr "Borrar"
-#: src/paths/admin/list/TableActive.tsx:99
-#, c-format
-msgid "add new instance"
+#: src/paths/admin/list/TableActive.tsx:100
+#, fuzzy, c-format
+msgid "Add new instance"
msgstr "agregar nueva instancia"
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr "ID"
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr "Nombre"
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr "Editar"
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr "Purgar"
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, 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
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr "Solo mostrar instancias activas"
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr "Activo"
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr "Mostrar solo instancias eliminadas"
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr "Eliminado"
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr "Mostrar todas las instancias"
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr "Todo"
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, 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
+#: src/paths/admin/list/index.tsx:105
#, 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"
+#: src/paths/admin/list/index.tsx:140
+#, fuzzy, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr "Instance '%1$s' (ID: %2$s) ha sido deshabilitada"
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr "Fallo al purgar la instancia"
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, 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:99
+#, 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:122
+#, fuzzy, c-format
+msgid "This is not a valid host."
+msgstr "Esta no es una dirección de bitcoin válida."
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Los números IBAN usualmente tienen mas de 4 digitos"
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Los números IBAN usualmente tienen menos de 34 digitos"
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código de pais de IBAN no encontrado"
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, fuzzy, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr "El número IBAN no es válido, falló la verificación"
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr "Elija uno..."
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr "Método a usar para la transferencia"
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr "Enrutamiento"
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr "Número de enrutamiento."
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr "Cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr "Numero de cuenta"
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr "Código"
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr "Código de identificación de la empresa."
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, fuzzy, c-format
+msgid "International Bank Account Number."
+msgstr "Número de cuenta bancaria."
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr "Interfaz de pago unificado."
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr "Protocolo Bitcoin."
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr "Protocolo Ethereum."
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr "Protocolo Interledger."
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr "Host del banco."
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr "Cuenta bancaria."
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, fuzzy, c-format
+msgid "Legal name of the person holding the account."
+msgstr "nombre de la unidad del producto"
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, fuzzy, c-format
+msgid "Invalid url"
+msgstr "formato inválido"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, fuzzy, c-format
+msgid "Server replied with \"bad request\"."
+msgstr "El servidor responde con un código de error"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, fuzzy, c-format
+msgid "Account:"
+msgstr "Cuenta"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, fuzzy, c-format
+msgid "Auth type"
+msgstr "Tipo de evento"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, fuzzy, c-format
+msgid "Do not change"
+msgstr "URL del proveedor"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, fuzzy, c-format
+msgid "Not verified"
+msgstr "Sin verificar"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr "Necesita completar los campos marcados"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, fuzzy, c-format
+msgid "Confirm operation"
+msgstr "Confirmado"
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, fuzzy, c-format
+msgid "Account details"
+msgstr "Dirección de cuenta"
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, fuzzy, c-format
+msgid "Could not create account"
+msgstr "no se pudo crear el producto"
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr "No se ha configurado una instancia por 'defecto' todavía."
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+"Cree una instancia \"por defecto\" para empezar a utilizar el backoffice "
+"comerciante."
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Bank accounts"
+msgstr "Cuenta bancaria"
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, fuzzy, c-format
+msgid "Add new account"
+msgstr "Abono en cuenta bancaria"
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, fuzzy, c-format
+msgid "Wire method: Bitcoin"
+msgstr "Método de transferencia"
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, fuzzy, c-format
+msgid "Delete selected accounts from the database"
+msgstr "eliminar transferencia seleccionada de la base de datos"
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, fuzzy, c-format
+msgid "Wire method: x-taler-bank"
+msgstr "Método de transferencia"
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, fuzzy, c-format
+msgid "Account name"
+msgstr "Dirección de la cuenta"
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, fuzzy, c-format
+msgid "Wire method: IBAN"
+msgstr "Método de transferencia"
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, fuzzy, c-format
+msgid "Other accounts"
+msgstr "Cuenta objetivo"
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, fuzzy, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr "No existen productos todavía, añadir más pulsando el símbolo +"
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, fuzzy, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr "URI que especifica la cuenta bancaria para acreditar los ingresos."
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, fuzzy, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr "utilizar la plantilla para crear un nuevo pedido"
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, fuzzy, c-format
+msgid "Bank account delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, fuzzy, c-format
+msgid "Could not delete the bank account"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, fuzzy, c-format
+msgid "Could not update account"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, fuzzy, c-format
+msgid "Could not delete account"
+msgstr "no se pudo eliminar el producto"
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -269,59 +874,109 @@ msgstr "Cuenta objetivo"
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
-msgstr "URL de KYC"
+msgid "Reason"
+msgstr "Razón"
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
-msgstr "Código"
+msgid "There is an anti-money laundering process pending to complete."
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr "Estado http"
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, 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"
+#: src/components/form/InputDate.tsx:127
+#, fuzzy, 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"
+#: src/components/form/InputDate.tsx:128
+#, fuzzy, c-format
+msgid "Change value to empty"
msgstr "cambiar valor a vacío"
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
+#, fuzzy, c-format
+msgid "Change value to never"
+msgstr "cambiar valor a nunca"
+
+#: src/components/form/InputDate.tsx:145
+#, fuzzy, c-format
+msgid "Never"
+msgstr "nunca"
+
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "clear"
-msgstr "limpiar"
+msgid "days"
+msgstr "días"
-#: src/components/form/InputDate.tsx:136
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "change value to never"
-msgstr "cambiar valor a nunca"
+msgid "hours"
+msgstr "horas"
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "never"
-msgstr "nunca"
+msgid "minutes"
+msgstr "minutos"
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr "segundos"
+
+#: src/components/form/InputDuration.tsx:62
+#, fuzzy, c-format
+msgid "Forever"
+msgstr "por siempre"
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr "%1$sM"
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr "%1$sA"
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr "%1$sd"
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr "%1$sh"
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr "%1$smin"
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr "%1$sseg"
#: 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"
@@ -362,43 +1017,33 @@ msgstr "Distrito"
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
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
-msgstr "Descripcion"
+msgstr "Descripción"
-#: src/components/form/InputSearchProduct.tsx:94
+#: src/components/form/InputSearchOnList.tsx:106
#, fuzzy, c-format
-msgid "Product"
-msgstr "Productos"
+msgid "Enter description or id"
+msgstr "Insertar un ID para el pedido"
-#: 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"
+#: src/components/form/InputSearchOnList.tsx:164
+#, fuzzy, c-format
+msgid "no match found with that description or id"
msgstr "no se encontraron productos con esa descripción"
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, 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
+#: src/components/product/InventoryProductForm.tsx:65
#, 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
+#: src/components/product/InventoryProductForm.tsx:77
+#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
@@ -406,24 +1051,29 @@ msgstr ""
"Esta cantidad excede las existencias restantes. Actualmente, solo quedan "
"%1$s unidades sin reservar en las existencias."
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, fuzzy, c-format
+msgid "Search product"
+msgstr "Productos de inventario"
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr "Cantidad"
-#: src/components/product/InventoryProductForm.tsx:110
-#, c-format
-msgid "how many products will be added"
+#: src/components/product/InventoryProductForm.tsx:113
+#, fuzzy, c-format
+msgid "How many products will be added"
msgstr "cuántos productos serán agregados"
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, 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"
+#, fuzzy, c-format
+msgid "Image must be smaller than 1 MB"
msgstr "La imagen debe ser mas chica que 1 MB"
#: src/components/form/InputImage.tsx:110
@@ -436,17 +1086,27 @@ msgstr "Agregar"
msgid "Remove"
msgstr "Eliminar"
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, fuzzy, c-format
+msgid "Invalid"
+msgstr "no válido"
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr "Ningun impuesto configurado para este producto."
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr "Monto"
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, c-format
msgid ""
"Taxes can be in currencies that differ from the main currency used by the "
@@ -455,51 +1115,61 @@ msgstr ""
"Impuestos pueden estar en divisas que difieren de la principal divisa usada "
"por el comerciante."
-#: src/components/form/InputTaxes.tsx:122
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, 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"
+#: src/components/form/InputTaxes.tsx:127
+#, fuzzy, 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"
+#: src/components/product/NonInventoryProductForm.tsx:71
+#, fuzzy, 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
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr "Agregue un producto personalizado"
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr "Complete información del producto"
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, fuzzy, c-format
+msgid "Must be a number"
+msgstr "no es un número"
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, fuzzy, c-format
+msgid "Must be grater than 0"
+msgstr "debe ser mayor que 0"
+
#: 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"
+#, fuzzy, c-format
+msgid "Photo of the product."
msgstr "foto del producto"
#: src/components/product/NonInventoryProductForm.tsx:192
-#, c-format
-msgid "full product description"
+#, fuzzy, c-format
+msgid "Full product description."
msgstr "descripción completa del producto"
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -508,8 +1178,8 @@ msgid "Unit"
msgstr "Unidad"
#: src/components/product/NonInventoryProductForm.tsx:197
-#, c-format
-msgid "name of the product unit"
+#, fuzzy, c-format
+msgid "Name of the product unit."
msgstr "nombre de la unidad del producto"
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -518,701 +1188,632 @@ msgid "Price"
msgstr "Precio"
#: src/components/product/NonInventoryProductForm.tsx:202
-#, c-format
-msgid "amount in the current currency"
+#, fuzzy, c-format
+msgid "Amount in the current currency."
msgstr "monto de la divisa actual"
+#: src/components/product/NonInventoryProductForm.tsx:208
+#, fuzzy, c-format
+msgid "How many products will be added."
+msgstr "cuántos productos serán agregados"
+
#: 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"
+#: src/components/product/ProductList.tsx:46
+#, fuzzy, 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
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "required"
-msgstr "requerido"
-
-#: src/paths/instance/orders/create/CreatePage.tsx:157
-#, c-format
-msgid "not valid"
-msgstr "no válido"
+msgid "Total price"
+msgstr "Precio total"
-#: src/paths/instance/orders/create/CreatePage.tsx:159
-#, c-format
-msgid "must be greater than 0"
+#: src/paths/instance/orders/create/CreatePage.tsx:162
+#, fuzzy, 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"
+#, fuzzy, c-format
+msgid "Refund deadline can't 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"
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:188
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:196
+#, fuzzy, c-format
+msgid "Must 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:201
+#, fuzzy, c-format
+msgid "Auto refund can't 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
+#: src/paths/instance/orders/create/CreatePage.tsx:208
+#, fuzzy, c-format
+msgid "Must be in the future"
+msgstr "deberían ser en el futuro"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:376
+#, c-format
+msgid "Simple"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:388
+#, c-format
+msgid "Advanced"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
msgid "Manage products in order"
msgstr "Manejar productos en orden"
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:404
+#, fuzzy, c-format
+msgid "%1$s products with a total price of %2$s."
+msgstr "actualizar el producto con nuevas existencias y precio"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, fuzzy, c-format
+msgid "Total product price added up"
msgstr "precio total de producto agregado"
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr "Precio de la orden"
-#: src/paths/instance/orders/create/CreatePage.tsx:437
-#, c-format
-msgid "final order price"
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Final order price"
msgstr "Precio final de la orden"
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr "Resumen"
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr "Envío y cumplimiento"
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr "Fecha de entrega"
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, 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"
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, fuzzy, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr "URL de cumplimiento"
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr "Opciones de pago de Taler"
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
-msgstr "Sobreescribir pagos por omisión de Taler para esta orden"
+msgstr "Sobrescribir pagos por omisión de Taler para esta orden"
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, fuzzy, c-format
-msgid "Payment deadline"
-msgstr "Plazo de pago"
+msgid "Payment time"
+msgstr "Opciones de pago"
-#: src/paths/instance/orders/create/CreatePage.tsx:482
-#, c-format
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, fuzzy, c-format
msgid ""
-"Deadline for the customer to pay for the offer before it expires. Inventory "
-"products will be reserved until this deadline."
+"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."
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:552
+#, fuzzy, c-format
+msgid "Default"
+msgstr "Importe por defecto"
-#: src/paths/instance/orders/create/CreatePage.tsx:487
-#, c-format
-msgid "Time until which the order can be refunded by the merchant."
+#: src/paths/instance/orders/create/CreatePage.tsx:561
+#, fuzzy, c-format
+msgid "Refund time"
+msgstr "Reembolsado"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:569
+#, fuzzy, c-format
+msgid ""
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
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:594
+#, fuzzy, c-format
+msgid "Wire transfer time"
+msgstr "ID 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:602
+#, fuzzy, c-format
+msgid ""
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
+msgstr "Plazo para que el proveedor haga la transferencia bancaria."
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, fuzzy, c-format
-msgid "Auto-refund deadline"
+msgid "Auto-refund time"
msgstr "Plazo de reembolso automático"
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Tiempo hasta el cual la cartera será automáticamente revisada por reembolsos "
+"sin interacció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:642
+#, fuzzy, c-format
+msgid "Maximum fee"
+msgstr "Máxima tarifa de transferencia"
-#: src/paths/instance/orders/create/CreatePage.tsx:503
-#, c-format
+#: src/paths/instance/orders/create/CreatePage.tsx:643
+#, fuzzy, 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."
+"Maximum 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
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, 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"
+msgstr "Crear token"
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
-#, fuzzy, c-format
+#: src/paths/instance/orders/create/CreatePage.tsx:656
+#, c-format
msgid "Minimum age required"
-msgstr "Login necesario"
+msgstr "Edad mínima requerida"
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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 ""
+"Cualquier valor superior a 0 limitará las monedas que se pueden utilizar "
+"para pagar este contrato. Si está vacío, la restricción de edad vendrá "
+"definida por los productos"
-#: src/paths/instance/orders/create/CreatePage.tsx:526
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, c-format
msgid "Min age defined by the producs is %1$s"
+msgstr "La edad mínima definida por el producto es%1$s"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:661
+#, c-format
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:534
-#, fuzzy, c-format
+#: src/paths/instance/orders/create/CreatePage.tsx:671
+#, c-format
msgid "Additional information"
-msgstr "Información extra"
+msgstr "Información adicional"
-#: src/paths/instance/orders/create/CreatePage.tsx:535
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
msgid "Custom information to be included in the contract for this order."
msgstr ""
+"Información personalizada que debe incluirse en el contrato para este pedido."
-#: src/paths/instance/orders/create/CreatePage.tsx:541
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
msgid "You must enter a value in JavaScript Object Notation (JSON)."
-msgstr ""
+msgstr "Debes introducir un valor en JavaScript Object Notation (JSON)."
-#: 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
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, fuzzy, c-format
-msgid "forever"
-msgstr "nunca"
+msgid "Custom field name"
+msgstr "Nombre de edificio"
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "%1$sM"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
-#, c-format
-msgid "%1$sY"
-msgstr ""
+#: src/paths/instance/orders/create/CreatePage.tsx:796
+#, fuzzy, c-format
+msgid "No deadline"
+msgstr "Plazo de reembolso"
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sd"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:68
-#, c-format
-msgid "%1$sh"
-msgstr ""
+#: src/paths/instance/orders/create/index.tsx:109
+#, fuzzy, c-format
+msgid "Could not create order"
+msgstr "no se pudo crear la reserva"
-#: src/components/form/InputDuration.tsx:70
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$smin"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:72
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$ssec"
+msgid "No more stock for product with id \"%1$s\"."
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:75
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr "Órdenes"
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, fuzzy, c-format
-msgid "create order"
-msgstr "creado"
+msgid "Create order"
+msgstr "crear orden"
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
-msgstr "cargar nuevas ordenes"
+msgid "Load first page"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr "Fecha"
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr "Devolución"
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, c-format
msgid "copy url"
msgstr "copiar url"
-#: src/paths/instance/orders/list/Table.tsx:225
+#: src/paths/instance/orders/list/Table.tsx:214
+#, fuzzy, c-format
+msgid "Load more orders after the last one"
+msgstr "cargue más transferencia luego de la última"
+
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "load older orders"
-msgstr "cargar viejas ordenes"
+msgid "Load next page"
+msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:242
+#: src/paths/instance/orders/list/Table.tsx:233
#, 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"
+#: src/paths/instance/orders/list/Table.tsx:280
+#, fuzzy, 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"
+#: src/paths/instance/orders/list/Table.tsx:293
+#, fuzzy, 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"
+#: src/paths/instance/orders/list/Table.tsx:381
+#, fuzzy, c-format
+msgid "Amount to be refunded"
msgstr "monto a ser reembolsado"
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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"
+#: src/paths/instance/orders/list/Table.tsx:391
+#, fuzzy, c-format
+msgid "Requested by the customer"
msgstr "pedido por el consumidor"
-#: src/paths/instance/orders/list/Table.tsx:400
-#, c-format
-msgid "other"
+#: src/paths/instance/orders/list/Table.tsx:392
+#, fuzzy, c-format
+msgid "Other"
msgstr "otro"
-#: src/paths/instance/orders/list/Table.tsx:403
-#, c-format
-msgid "why this order is being refunded"
+#: src/paths/instance/orders/list/Table.tsx:395
+#, fuzzy, 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"
+#: src/paths/instance/orders/list/Table.tsx:401
+#, fuzzy, c-format
+msgid "More information to give context"
msgstr "más información para dar contexto"
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, 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"
+#: src/paths/instance/orders/details/DetailPage.tsx:78
+#, fuzzy, 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"
+#: src/paths/instance/orders/details/DetailPage.tsx:84
+#, fuzzy, c-format
+msgid "Total price for the transaction"
msgstr "precio total de la transacción"
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr "URL para esta compra"
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, 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"
+#: src/paths/instance/orders/details/DetailPage.tsx:98
+#, fuzzy, c-format
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
+"tasa máxima total de depósito aceptada por el comerciante para este contrato"
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, 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:104
+#, fuzzy, c-format
+msgid "Time when this contract was generated"
+msgstr "momento en que se generó este contrato"
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
-msgstr ""
+msgid "Refund deadline"
+msgstr "Plazo de reembolso"
-#: src/paths/instance/orders/details/DetailPage.tsx:118
-#, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:110
+#, fuzzy, c-format
+msgid "After this deadline has passed no refunds will be accepted"
+msgstr "pasado este plazo no se aceptarán devoluciones"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:115
+#, fuzzy, c-format
+msgid "Payment deadline"
+msgstr "Plazo de pago"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:116
+#, fuzzy, c-format
msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
-msgstr ""
+"After this deadline, the merchant won't accept payments for the contract"
+msgstr "pasado este plazo, el comerciante no aceptará pagos por el contrato"
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid "transfer deadline for the exchange"
-msgstr ""
+msgid "Wire transfer deadline"
+msgstr "Plazo de la transferencia bancaria"
-#: 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:122
+#, fuzzy, c-format
+msgid "Transfer deadline for the exchange"
+msgstr "plazo de transferencia para el proveedor"
-#: 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:128
+#, fuzzy, c-format
+msgid "Time indicating when the order should be delivered"
+msgstr "fecha en la que debe entregarse el pedido"
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, fuzzy, c-format
+msgid "Where the order will be delivered"
+msgstr "dónde se entregará el pedido"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:142
+#, c-format
msgid "Auto-refund delay"
msgstr "Plazo de reembolso automático"
-#: src/paths/instance/orders/details/DetailPage.tsx:145
-#, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:143
+#, fuzzy, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
+"cuánto tiempo debe intentar la cartera obtener el reembolso automático de la "
+"compra"
-#: src/paths/instance/orders/details/DetailPage.tsx:150
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:148
+#, c-format
msgid "Extra info"
-msgstr "Información extra"
+msgstr "Información adicional"
-#: src/paths/instance/orders/details/DetailPage.tsx:151
-#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+#: src/paths/instance/orders/details/DetailPage.tsx:149
+#, fuzzy, c-format
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
+"datos adicionales que solo son interpretados por la interfaz del comerciante"
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr "Orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:221
-#, c-format
-msgid "claimed"
+#: src/paths/instance/orders/details/DetailPage.tsx:224
+#, fuzzy, c-format
+msgid "Claimed"
msgstr "reclamado"
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, fuzzy, c-format
-msgid "claimed at"
-msgstr "reclamado"
+msgid "Claimed at"
+msgstr "reclamado en"
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr "Cronología"
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr "Detalles de pago"
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr "Estado de orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr "Lista de producto"
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
-msgstr "pagados"
+msgid "Paid"
+msgstr "Pagado"
-#: src/paths/instance/orders/details/DetailPage.tsx:455
-#, c-format
-msgid "wired"
+#: src/paths/instance/orders/details/DetailPage.tsx:465
+#, fuzzy, c-format
+msgid "Wired"
msgstr "transferido"
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
-msgstr "reembolzado"
+msgid "Refunded"
+msgstr "Reembolsado"
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/details/DetailPage.tsx:490
#, fuzzy, c-format
-msgid "refund order"
-msgstr "reembolzado"
+msgid "Refund order"
+msgstr "reembolsado"
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, fuzzy, c-format
-msgid "not refundable"
-msgstr "Máximo reembolzable:"
+msgid "Not refundable"
+msgstr "No reembolsable"
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
-msgstr "reembolzar"
+msgid "Next event in"
+msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
-msgstr "Monto reembolzado"
+msgstr "Monto reembolsado"
-#: src/paths/instance/orders/details/DetailPage.tsx:560
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:564
+#, c-format
msgid "Refund taken"
-msgstr "Reembolzado"
+msgstr "Reembolsado"
-#: src/paths/instance/orders/details/DetailPage.tsx:570
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:574
+#, c-format
msgid "Status URL"
-msgstr "URL de estado de orden"
+msgstr "Estado de la URL"
-#: src/paths/instance/orders/details/DetailPage.tsx:583
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:587
+#, c-format
msgid "Refund URI"
-msgstr "Devolución"
+msgstr "URI de devolución"
-#: src/paths/instance/orders/details/DetailPage.tsx:636
-#, c-format
-msgid "unpaid"
+#: src/paths/instance/orders/details/DetailPage.tsx:641
+#, fuzzy, c-format
+msgid "Unpaid"
msgstr "impago"
-#: src/paths/instance/orders/details/DetailPage.tsx:654
-#, c-format
-msgid "pay at"
+#: src/paths/instance/orders/details/DetailPage.tsx:659
+#, fuzzy, 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
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr "URL de estado de orden"
-#: src/paths/instance/orders/details/DetailPage.tsx:711
-#, fuzzy, c-format
+#: src/paths/instance/orders/details/DetailPage.tsx:716
+#, c-format
msgid "Payment URI"
msgstr "URI de pago"
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
@@ -1220,109 +1821,257 @@ msgstr ""
"Estado de orden desconocido. Esto es un error, por favor contacte a su "
"administrador."
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
msgid "Back"
+msgstr "Regresar"
+
+#: src/paths/instance/orders/details/index.tsx:88
+#, fuzzy, c-format
+msgid "Refund created successfully"
+msgstr "reembolzo creado satisfactoriamente"
+
+#: src/paths/instance/orders/details/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not create the refund"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/orders/details/index.tsx:97
+#, c-format
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "refund created successfully"
-msgstr "reembolzo creado satisfactoriamente"
+msgid "Missing id"
+msgstr ""
+
+#: src/components/form/JumpToElementById.tsx:48
+#, fuzzy, c-format
+msgid "Not found"
+msgstr "Orden no encontrada"
+
+#: src/paths/instance/orders/list/ListPage.tsx:83
+#, fuzzy, c-format
+msgid "Select date to show nearby orders"
+msgstr "seleccione la fecha para mostrar pedidos cercanos"
+
+#: src/paths/instance/orders/list/ListPage.tsx:96
+#, fuzzy, c-format
+msgid "Only show paid orders"
+msgstr "mostrar sólo pedidos pagados"
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "could not create the refund"
-msgstr "No se pudo create el reembolso"
+msgid "New"
+msgstr "Nuevo"
+
+#: src/paths/instance/orders/list/ListPage.tsx:116
+#, fuzzy, c-format
+msgid "Only show orders with refunds"
+msgstr "mostrar solo pedidos con reembolso"
+
+#: src/paths/instance/orders/list/ListPage.tsx:126
+#, fuzzy, c-format
+msgid ""
+"Only show orders where customers paid, but wire payments from payment "
+"provider are still pending"
+msgstr ""
+"mostrar sólo los pedidos en los que los clientes han pagado, pero los pagos "
+"por transferencia del proveedor de pago siguen pendientes"
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
-msgid "select date to show nearby orders"
+msgid "Not wired"
+msgstr "No transferido"
+
+#: src/paths/instance/orders/list/ListPage.tsx:139
+#, fuzzy, c-format
+msgid "Completed"
+msgstr "Eliminado"
+
+#: src/paths/instance/orders/list/ListPage.tsx:146
+#, fuzzy, c-format
+msgid "Remove all filters"
+msgstr "eliminar todos los filtros"
+
+#: src/paths/instance/orders/list/ListPage.tsx:164
+#, fuzzy, c-format
+msgid "Clear date filter"
+msgstr "borrar filtro de fechas"
+
+#: src/paths/instance/orders/list/ListPage.tsx:178
+#, c-format
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/paths/instance/orders/list/index.tsx:113
+#, fuzzy, c-format
+msgid "Jump to order with the given product ID"
+msgstr "saltar al pedido con el ID de pedido proporcionado"
+
+#: src/paths/instance/orders/list/index.tsx:114
#, fuzzy, c-format
-msgid "order id"
-msgstr "ir a id de orden"
+msgid "Order id"
+msgstr "ID de la orden"
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
+#, fuzzy, c-format
+msgid "Just letters and numbers from 2 to 7"
+msgstr "sólo letras y números del 2 al 7"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
+#, fuzzy, c-format
+msgid "Size of the key must be 32"
+msgstr "el tamaño de la clave debe ser 32"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "remove all filters"
+msgid "Internal id on the system"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "only show paid orders"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Paid"
-msgstr "Pagado"
+msgid "Verification algorithm"
+msgstr "Algoritmo de verificación"
-#: 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/otp_devices/create/CreatePage.tsx:109
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr "Algoritmo a utilizar para verificar la transacción en modo offline"
-#: src/paths/instance/orders/list/ListPage.tsx:145
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Refunded"
-msgstr "Reembolsado"
+msgid "Device key"
+msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:152
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid ""
-"only show orders where customers paid, but wire payments from payment "
-"provider are still pending"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "Not wired"
-msgstr "No transferido"
+msgid "Your device need to have exactly the same value"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
+#, fuzzy, c-format
+msgid "Generate random secret key"
+msgstr "generar clave secreta aleatoria"
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
+#, fuzzy, c-format
+msgid "Random"
+msgstr "aleatorio"
+
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "clear date filter"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/otp_devices/create/index.tsx:60
+#, fuzzy, c-format
+msgid "Device added successfully"
+msgstr "reembolzo creado satisfactoriamente"
+
+#: src/paths/instance/otp_devices/create/index.tsx:66
+#, fuzzy, c-format
+msgid "Could not add device"
+msgstr "no se pudo crear la reserva"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "OTP Devices"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/otp_devices/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Add new devices"
+msgstr "cargar nuevas transferencias"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, fuzzy, c-format
-msgid "Enter an order id"
-msgstr "ir a id de orden"
+msgid "Load more devices before the first one"
+msgstr "cargar más plantillas antes de la primera"
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, fuzzy, c-format
-msgid "order not found"
-msgstr "Servidor no encontrado"
+msgid "Delete selected devices from the database"
+msgstr "eliminar la reserva seleccionada de la base de datos"
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, fuzzy, c-format
-msgid "could not get the order to refund"
-msgstr "No se pudo create el reembolso"
+msgid "Load more devices after the last one"
+msgstr "cargar más plantillas después de la última"
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, fuzzy, c-format
-msgid "Loading..."
-msgstr "Cargando..."
+msgid "There is no devices yet, add more pressing the + sign"
+msgstr "Todavía no hay instancias, agregue más presionando el signo +"
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/list/index.tsx:90
+#, fuzzy, c-format
+msgid "Device delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/otp_devices/list/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not delete the device"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
+#, c-format
+msgid "Device:"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
+#, fuzzy, c-format
+msgid "Not modified"
+msgstr "No transferido"
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
+#, c-format
+msgid "Change key"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/index.tsx:119
+#, fuzzy, c-format
+msgid "Could not update template"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/otp_devices/update/index.tsx:121
+#, c-format
+msgid "Template id is unknown"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
msgid ""
-"click here to configure the stock of the product, leave it as is and the "
-"backend will not control stock"
+"The provided information is inconsistent with the current state of the "
+"template"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, fuzzy, c-format
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock."
msgstr ""
+"pulse aquí para configurar el stock del producto, déjelo como está y el "
+"backend no controlará el stock"
#: src/components/form/InputStock.tsx:109
#, c-format
@@ -1330,9 +2079,9 @@ msgid "Manage stock"
msgstr "Administrar stock"
#: src/components/form/InputStock.tsx:115
-#, c-format
-msgid "this product has been configured without stock control"
-msgstr ""
+#, fuzzy, c-format
+msgid "This product has been configured without stock control"
+msgstr "este producto se ha configurado sin control de existencias"
#: src/components/form/InputStock.tsx:119
#, c-format
@@ -1341,1430 +2090,2057 @@ 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 )"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
+msgstr ""
+"la pérdida no puede ser mayor que la cantidad entrante actual (max %1$s )"
-#: src/components/form/InputStock.tsx:176
+#: src/components/form/InputStock.tsx:169
#, c-format
msgid "Incoming"
msgstr "Ingresando"
-#: src/components/form/InputStock.tsx:177
+#: src/components/form/InputStock.tsx:170
#, c-format
msgid "Lost"
msgstr "Perdido"
-#: src/components/form/InputStock.tsx:192
+#: src/components/form/InputStock.tsx:185
#, 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:189
+#, fuzzy, c-format
+msgid "Remove stock control for this product"
+msgstr "eliminar el control de existencias de este producto"
-#: src/components/form/InputStock.tsx:202
+#: src/components/form/InputStock.tsx:195
#, c-format
msgid "without stock"
msgstr "sin stock"
-#: src/components/form/InputStock.tsx:211
+#: src/components/form/InputStock.tsx:204
#, 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/form/InputStock.tsx:208
+#, fuzzy, c-format
+msgid "Warehouse address"
+msgstr "Dirección de cuenta"
-#: src/components/product/ProductForm.tsx:133
-#, c-format
-msgid "product identification to use in URLs (for internal use only)"
-msgstr ""
+#: src/components/form/InputArray.tsx:118
+#, fuzzy, c-format
+msgid "Add element to the list"
+msgstr "agregar elemento a la lista"
-#: src/components/product/ProductForm.tsx:139
-#, c-format
-msgid "illustration of the product for customers"
-msgstr ""
+#: src/components/product/ProductForm.tsx:120
+#, fuzzy, c-format
+msgid "Invalid amount"
+msgstr "Importe fijo"
-#: src/components/product/ProductForm.tsx:145
-#, c-format
-msgid "product description for customers"
+#: src/components/product/ProductForm.tsx:181
+#, fuzzy, c-format
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
+"Identificación del producto para usar en las URL (solo para uso interno)"
-#: src/components/product/ProductForm.tsx:149
-#, c-format
-msgid "Age restricted"
+#: src/components/product/ProductForm.tsx:187
+#, fuzzy, c-format
+msgid "Illustration of the product for customers."
+msgstr "ilustración del producto para los clientes"
+
+#: src/components/product/ProductForm.tsx:193
+#, fuzzy, c-format
+msgid "Product description for customers."
+msgstr "descripción del producto para los clientes"
+
+#: src/components/product/ProductForm.tsx:197
+#, fuzzy, c-format
+msgid "Age restriction"
+msgstr "Restricción de edad"
+
+#: src/components/product/ProductForm.tsx:198
+#, fuzzy, c-format
+msgid "Is this product restricted for customer below certain age?"
+msgstr "¿este producto está restringido para clientes menores de cierta edad?"
+
+#: src/components/product/ProductForm.tsx:199
+#, fuzzy, c-format
+msgid "Minimum age of the customer"
+msgstr "Edad mínima requerida"
+
+#: src/components/product/ProductForm.tsx:203
+#, fuzzy, c-format
+msgid "Unit name"
+msgstr "Unidad"
+
+#: src/components/product/ProductForm.tsx:204
+#, fuzzy, c-format
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
msgstr ""
+"unidad que describe la cantidad de producto vendido (por ejemplo, 2 "
+"kilogramos, 5 litros, 3 artículos, 5 metros) para los clientes"
-#: src/components/product/ProductForm.tsx:150
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+msgid "Price per unit"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
-#, c-format
+#: src/components/product/ProductForm.tsx:210
+#, fuzzy, c-format
msgid ""
-"sale price for customers, including taxes, for above units of the product"
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
+"precio de venta para los clientes, impuestos incluidos, por encima de las "
+"unidades del producto"
-#: src/components/product/ProductForm.tsx:164
+#: src/components/product/ProductForm.tsx:214
#, 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)"
+#: src/components/product/ProductForm.tsx:216
+#, fuzzy, c-format
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
+"inventario de productos para productos con suministro finito (sólo para uso "
+"interno)"
-#: src/components/product/ProductForm.tsx:171
-#, c-format
-msgid "taxes included in the product price, exposed to customers"
+#: src/components/product/ProductForm.tsx:221
+#, fuzzy, c-format
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
+"impuestos incluidos en el precio del producto, expuestos a los clientes"
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Need to complete marked fields"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
-#, c-format
-msgid "could not create product"
-msgstr "no se pudo crear el producto"
+#: src/components/product/ProductForm.tsx:231
+#, fuzzy, c-format
+msgid "Search by category description or id"
+msgstr "buscar productos por su descripción o ID"
-#: src/paths/instance/products/list/Table.tsx:68
-#, c-format
-msgid "Products"
-msgstr "Productos"
+#: src/components/product/ProductForm.tsx:232
+#, fuzzy, c-format
+msgid "Categories where this product will be listed on."
+msgstr "dirección a donde los productos serán entregados"
+
+#: src/paths/instance/products/create/index.tsx:52
+#, fuzzy, c-format
+msgid "Product created successfully"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create product"
+msgstr "no se pudo crear el producto"
#: src/paths/instance/products/list/Table.tsx:73
-#, c-format
-msgid "add product to inventory"
-msgstr ""
+#, fuzzy, c-format
+msgid "Inventory"
+msgstr "Productos de inventario"
-#: src/paths/instance/products/list/Table.tsx:137
-#, c-format
-msgid "Sell"
-msgstr "Venta"
+#: src/paths/instance/products/list/Table.tsx:78
+#, fuzzy, c-format
+msgid "Add product to inventory"
+msgstr "añadir producto al inventario"
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Profit"
-msgstr "Ganancia"
+msgid "Sales"
+msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
msgid "Sold"
msgstr "Vendido"
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "free"
-msgstr "Gratis"
+msgid "Free"
+msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/paths/instance/products/list/Table.tsx:271
#, fuzzy, c-format
-msgid "go to product update page"
-msgstr "producto actualizado correctamente"
+msgid "Go to product update page"
+msgstr "ir a la página de actualización del producto"
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
msgid "Update"
msgstr "Actualizar"
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/paths/instance/products/list/Table.tsx:283
+#, fuzzy, c-format
+msgid "Remove this product from the database"
+msgstr "eliminar este producto de la base de datos"
+
+#: src/paths/instance/products/list/Table.tsx:318
+#, fuzzy, c-format
+msgid "Load more products after the last one"
+msgstr "cargar más plantillas después de la última"
+
+#: src/paths/instance/products/list/Table.tsx:361
+#, fuzzy, c-format
+msgid "Update the product with new price"
+msgstr "actualizar el producto con el nuevo precio"
+
+#: src/paths/instance/products/list/Table.tsx:373
+#, fuzzy, c-format
+msgid "Update product with new price"
+msgstr "actualizar producto con nuevo precio"
+
+#: src/paths/instance/products/list/Table.tsx:384
+#, fuzzy, c-format
+msgid "Confirm update"
+msgstr "Confirmado"
+
+#: src/paths/instance/products/list/Table.tsx:431
+#, fuzzy, c-format
+msgid "Add more elements to the inventory"
+msgstr "añadir más elementos al inventario"
+
+#: src/paths/instance/products/list/Table.tsx:436
+#, fuzzy, c-format
+msgid "Report elements lost in the inventory"
+msgstr "informar de elementos perdidos en el inventario"
+
+#: src/paths/instance/products/list/Table.tsx:441
+#, fuzzy, c-format
+msgid "New price for the product"
+msgstr "nuevo precio para el producto"
+
+#: src/paths/instance/products/list/Table.tsx:453
+#, fuzzy, c-format
+msgid "The are value with errors"
+msgstr "hay valores con errores"
+
+#: src/paths/instance/products/list/Table.tsx:454
+#, fuzzy, c-format
+msgid "Update product with new stock and price"
+msgstr "actualizar el producto con nuevas existencias y precio"
+
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "remove this product from the database"
-msgstr ""
+msgid "There is no products yet, add more pressing the + sign"
+msgstr "No existen productos todavía, añadir más pulsando el símbolo +"
+
+#: src/paths/instance/products/list/index.tsx:86
+#, fuzzy, c-format
+msgid "Jump to product with the given product ID"
+msgstr "saltar al pedido con el ID de pedido proporcionado"
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "update the product with new price"
-msgstr ""
+msgid "Product id"
+msgstr "Id de producto"
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/paths/instance/products/list/index.tsx:104
+#, fuzzy, c-format
+msgid "Product updated successfully"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/products/list/index.tsx:109
+#, fuzzy, c-format
+msgid "Could not update the product"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/products/list/index.tsx:144
+#, fuzzy, c-format
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/instance/products/list/index.tsx:149
+#, fuzzy, c-format
+msgid "Could not delete the product"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "update product with new price"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "add more elements to the inventory"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/paths/instance/products/update/UpdatePage.tsx:56
#, c-format
-msgid "report elements lost in the inventory"
-msgstr ""
+msgid "Product id:"
+msgstr "ID de producto:"
+
+#: src/paths/instance/products/update/index.tsx:85
+#, fuzzy, c-format
+msgid "Product (ID: %1$s) has been updated"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/paths/instance/products/update/index.tsx:91
#, fuzzy, c-format
-msgid "new price for the product"
+msgid "Could not update product"
msgstr "no se pudo actualizar el producto"
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/paths/instance/templates/create/CreatePage.tsx:96
#, c-format
-msgid "the are value with errors"
+msgid "Invalid. only characters and numbers"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
-#, c-format
-msgid "update product with new stock and price"
-msgstr ""
+#: src/paths/instance/templates/create/CreatePage.tsx:112
+#, fuzzy, c-format
+msgid "Must be greater that 0"
+msgstr "debe ser mayor que 0"
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/paths/instance/templates/create/CreatePage.tsx:119
#, 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 +"
+msgid "To short"
+msgstr "demasiado corta"
-#: src/paths/instance/products/list/index.tsx:86
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
-msgid "product updated successfully"
-msgstr "producto actualizado correctamente"
+msgid "Identifier"
+msgstr "Identificador"
-#: src/paths/instance/products/list/index.tsx:92
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
-msgid "could not update the product"
-msgstr "no se pudo actualizar el producto"
+msgid "Name of the template in URLs."
+msgstr "Nombre de la plantilla en las URL."
-#: src/paths/instance/products/list/index.tsx:103
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
-msgid "product delete successfully"
-msgstr "producto fue eliminado correctamente"
+msgid "Describe what this template stands for"
+msgstr "Describa lo que representa esta plantilla"
-#: src/paths/instance/products/list/index.tsx:109
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "could not delete the product"
-msgstr "no se pudo eliminar el producto"
+msgid "If specified, this template will create order with the same summary"
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo resumen"
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/paths/instance/templates/create/CreatePage.tsx:210
+#, c-format
+msgid "Summary is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:211
+#, c-format
+msgid "Allow the user to change the summary."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:217
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo precio"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:221
#, fuzzy, c-format
-msgid "Product id:"
-msgstr "Id de producto"
+msgid "Amount is editable"
+msgstr "Monto abonado"
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/paths/instance/templates/create/CreatePage.tsx:222
#, 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."
+msgid "Allow the user to select the amount to pay."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/paths/instance/templates/create/CreatePage.tsx:229
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Currency is editable"
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/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/paths/instance/templates/create/CreatePage.tsx:232
#, c-format
-msgid "must be a valid URL"
+msgid "Supported currencies"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
+#, c-format
+msgid "Minimum age"
+msgstr "Edad mínima"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:243
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr "¿Este contrato está restringido a alguna edad?"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, fuzzy, c-format
-msgid "Initial balance"
-msgstr "Instancia"
+msgid "Payment timeout"
+msgstr "Opciones de pago"
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, c-format
-msgid "balance prior to deposit"
+msgid ""
+"How much time has the customer to complete the payment once the order was "
+"created."
msgstr ""
+"Cuánto tiempo tiene el cliente para completar el pago una vez creado el "
+"pedido."
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Exchange URL"
-msgstr "URL del Exchange"
+msgid "OTP device"
+msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/paths/instance/templates/create/CreatePage.tsx:255
+#, fuzzy, c-format
+msgid "Use to verify transaction while offline."
+msgstr "Algoritmo a utilizar para verificar la transacción en modo offline"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "URL of exchange"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Next"
-msgstr "Siguiente"
+msgid "Add one first"
+msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "Wire method"
+msgid "No device"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, fuzzy, c-format
-msgid "method to use for wire transfer"
-msgstr "no se pudo informar la transferencia"
+msgid "Use to verify transaction in offline mode."
+msgstr "Algoritmo a utilizar para verificar la transacción en modo offline"
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "Select one wire method"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/paths/instance/templates/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create template"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/templates/list/Table.tsx:61
+#, c-format
+msgid "Templates"
+msgstr "Plantillas"
+
+#: src/paths/instance/templates/list/Table.tsx:66
#, fuzzy, c-format
-msgid "could not create reserve"
+msgid "Add new templates"
+msgstr "añadir nuevas plantillas"
+
+#: src/paths/instance/templates/list/Table.tsx:127
+#, fuzzy, c-format
+msgid "Load more templates before the first one"
+msgstr "cargar más plantillas antes de la primera"
+
+#: src/paths/instance/templates/list/Table.tsx:165
+#, fuzzy, c-format
+msgid "Delete selected templates from the database"
+msgstr "eliminar las plantillas seleccionadas de la base de datos"
+
+#: src/paths/instance/templates/list/Table.tsx:172
+#, fuzzy, c-format
+msgid "Use template to create new order"
+msgstr "utilizar la plantilla para crear un nuevo pedido"
+
+#: src/paths/instance/templates/list/Table.tsx:175
+#, fuzzy, c-format
+msgid "Use template"
+msgstr "añadir nuevas plantillas"
+
+#: src/paths/instance/templates/list/Table.tsx:179
+#, fuzzy, c-format
+msgid "Create qr code for the template"
msgstr "No se pudo create el reembolso"
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/paths/instance/templates/list/Table.tsx:194
+#, fuzzy, c-format
+msgid "Load more templates after the last one"
+msgstr "cargar más plantillas después de la última"
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, 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:91
+#, fuzzy, c-format
+msgid "Jump to template with the given template ID"
+msgstr "saltar al pedido con el ID de pedido proporcionado"
+
+#: src/paths/instance/templates/list/index.tsx:92
#, c-format
-msgid "Valid until"
-msgstr "Válido hasta"
+msgid "Template identification"
+msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/templates/list/index.tsx:132
#, fuzzy, c-format
-msgid "Created balance"
-msgstr "creado"
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/templates/list/index.tsx:137
#, fuzzy, c-format
-msgid "Exchange balance"
-msgstr "Monto inicial"
+msgid "Failed to delete template"
+msgstr "Fallo al eliminar instancia"
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Picked up"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/templates/list/index.tsx:160
#, fuzzy, c-format
-msgid "Committed"
-msgstr "Monto confirmado"
+msgid "Deleting an template"
+msgstr "cargar nuevas transferencias"
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/templates/list/index.tsx:162
+#, fuzzy, c-format
+msgid "can't be undone"
+msgstr "no puede ser vacío"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
-msgid "Account address"
-msgstr "Dirección de cuenta"
+msgid "Print"
+msgstr "Imprimir"
+
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
+#, fuzzy, c-format
+msgid "Too short"
+msgstr "demasiado corta"
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/templates/update/index.tsx:90
+#, fuzzy, c-format
+msgid "Template (ID: %1$s) has been updated"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, fuzzy, c-format
+msgid "Amount is required"
+msgstr "Login necesario"
+
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "Subject"
-msgstr "Asunto"
+msgid "Order summary is required"
+msgstr "Se requiere resumen del pedido"
+
+#: 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 "Importe del pedido"
+
+#: 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:125
+#, fuzzy, c-format
+msgid "Could not create order from template"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/token/DetailPage.tsx:57
+#, fuzzy, c-format
+msgid "You need your access token to perform the operation"
+msgstr "Está estableciendo el token de acceso para la nueva instancia"
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/token/DetailPage.tsx:74
+#, fuzzy, 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/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "Tips"
-msgstr "Propinas"
+msgid "This instance doesn't have authentication token."
+msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/token/DetailPage.tsx:121
#, fuzzy, c-format
-msgid "Authorized"
-msgstr "Token de autorización"
+msgid "Current access token"
+msgstr "Establecer token de acceso"
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/token/DetailPage.tsx:126
#, fuzzy, c-format
-msgid "Expiration"
-msgstr "Información extra"
+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/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/token/DetailPage.tsx:142
#, fuzzy, c-format
-msgid "amount of tip"
-msgstr "monto"
+msgid "Clear token"
+msgstr "Crear token"
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/token/DetailPage.tsx:177
#, fuzzy, c-format
-msgid "Justification"
-msgstr "Jurisdicción"
+msgid "Confirm change"
+msgstr "Confirmado"
+
+#: src/paths/instance/token/index.tsx:83
+#, fuzzy, c-format
+msgid "Failed to clear token"
+msgstr "Fallo al crear la instancia"
+
+#: src/paths/instance/token/index.tsx:109
+#, fuzzy, c-format
+msgid "Failed to set new token"
+msgstr "Fallo al eliminar instancia"
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "reason for the tip"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
+#, fuzzy, c-format
+msgid "Token family slug to use in URLs (for internal use only)"
+msgstr ""
+"Identificación del producto para usar en las URL (solo para uso interno)"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "URL after tip"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
+#, c-format
+msgid "User-readable token family name"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, fuzzy, c-format
-msgid "Reserves not yet funded"
-msgstr "Servidor no encontrado"
+msgid "Token family description for customers"
+msgstr "descripción del producto para los clientes"
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
+#, fuzzy, c-format
+msgid "Valid After"
+msgstr "Válido hasta"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Reserves ready"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, fuzzy, c-format
-msgid "add new reserve"
-msgstr "cargar nuevas transferencias"
+msgid "Valid Before"
+msgstr "formato inválido"
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "Expires at"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
+#, fuzzy, c-format
+msgid "Duration"
+msgstr "Expiración"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "Initial"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
+#, fuzzy, c-format
+msgid "Token familty created successfully"
+msgstr "reembolzo creado satisfactoriamente"
+
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
+#, fuzzy, c-format
+msgid "Could not create token family"
+msgstr "No se pudo create el reembolso"
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, 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 +"
+msgid "Go to token family update page"
+msgstr "ir a la página de actualización del producto"
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
#, fuzzy, c-format
-msgid "Expected Balance"
-msgstr "Ejecutado en"
+msgid "Remove this token family from the database"
+msgstr "eliminar este producto de la base de datos"
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
#, fuzzy, c-format
-msgid "could not create the tip"
-msgstr "No se pudo create el reembolso"
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
#, fuzzy, c-format
-msgid "should not be empty"
-msgstr "no puede ser vacío"
+msgid "Token family updated successfully"
+msgstr "producto actualizado correctamente"
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
#, fuzzy, c-format
-msgid "should be greater that 0"
-msgstr "Debe ser mayor a 0"
+msgid "Could not update the token family"
+msgstr "no se pudo actualizar el producto"
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
#, fuzzy, c-format
-msgid "can't be empty"
-msgstr "no puede ser vacío"
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, fuzzy, c-format
+msgid "Failed to delete token family"
+msgstr "Fallo al eliminar instancia"
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
#, c-format
-msgid "to short"
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Deleting a token family %1$s ."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
#, c-format
-msgid "size of the key should be 32"
+msgid "Token Family: %1$s"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, fuzzy, c-format
+msgid "Token familty updated successfully"
+msgstr "producto actualizado correctamente"
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
+#, fuzzy, c-format
+msgid "Could not update token family"
+msgstr "no se pudo actualizar el producto"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
+#, fuzzy, c-format
+msgid "Check the id, does not look valid"
+msgstr "comprueba el ID, parece no ser válido"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
+#, fuzzy, c-format
+msgid "Must have 52 characters, current %1$s"
+msgstr "debería tener 52 caracteres, actualmente %1$s"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
-msgid "Identifier"
-msgstr ""
+msgid "URL doesn't have the right format"
+msgstr "La URL no tiene el formato correcto"
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
-msgid "Name of the template in URLs."
-msgstr ""
+msgid "Credited bank account"
+msgstr "Abono en cuenta bancaria"
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
-msgid "Describe what this template stands for"
-msgstr ""
+msgid "Select one account"
+msgstr "Selecciona una cuenta"
-#: src/paths/instance/templates/create/CreatePage.tsx:149
-#, fuzzy, c-format
-msgid "Fixed summary"
-msgstr "Estado de orden"
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr "Cuenta bancaria del comerciante donde se recibió el pago"
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
-msgid "If specified, this template will create order with the same summary"
-msgstr ""
+msgid "Wire transfer ID"
+msgstr "ID de la transferencia"
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, fuzzy, c-format
-msgid "Fixed price"
-msgstr "precio unitario"
+msgid ""
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+"identificador único de la transferencia utilizado por el proveedor, debe "
+"tener 52 caracteres"
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
#, c-format
-msgid "If specified, this template will create order with the same price"
+msgid "Exchange URL"
+msgstr "URL del proveedor"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
msgstr ""
+"URL base del proveedor que realizó la transferencia, debería haber estado en "
+"el asunto de la transferencia bancaria"
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
-msgid "Minimum age"
-msgstr "Edad mínima"
+msgid "Amount credited"
+msgstr "Monto abonado"
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
-msgid "Is this contract restricted to some age?"
-msgstr ""
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr "Monto real que se transfirió a la cuenta bancaria del comerciante"
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/transfers/create/index.tsx:62
#, fuzzy, c-format
-msgid "Payment timeout"
-msgstr "Opciones de pago"
+msgid "Wire transfer informed successfully"
+msgstr "reembolzo creado satisfactoriamente"
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/transfers/create/index.tsx:68
+#, fuzzy, c-format
+msgid "Could not inform transfer"
+msgstr "no se pudo informar la transferencia"
+
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid ""
-"How much time has the customer to complete the payment once the order was "
-"created."
-msgstr ""
+msgid "Transfers"
+msgstr "Transferencias"
+
+#: src/paths/instance/transfers/list/Table.tsx:64
+#, fuzzy, c-format
+msgid "Add new transfer"
+msgstr "añadir nueva transferencia"
-#: src/paths/instance/templates/create/CreatePage.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:117
+#, fuzzy, c-format
+msgid "Load more transfers before the first one"
+msgstr "cargar más transferencias antes de la primera"
+
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
-msgid "Verification algorithm"
-msgstr ""
+msgid "Credit"
+msgstr "Crédito"
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
-msgstr ""
+msgid "Confirmed"
+msgstr "Confirmado"
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
-msgid "Point-of-sale key"
-msgstr ""
+msgid "Verified"
+msgstr "Verificado"
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
-msgid "Useful to validate the purchase"
-msgstr ""
+msgid "Executed at"
+msgstr "Ejecutado en"
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
-msgid "generate random secret key"
-msgstr ""
+msgid "yes"
+msgstr "si"
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
-msgid "random"
-msgstr ""
+msgid "no"
+msgstr "no"
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "show secret key"
-msgstr ""
+msgid "never"
+msgstr "nunca"
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "hide secret key"
-msgstr ""
+msgid "unknown"
+msgstr "desconocido"
+
+#: src/paths/instance/transfers/list/Table.tsx:166
+#, fuzzy, 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:181
+#, fuzzy, c-format
+msgid "Load more transfers after the last one"
+msgstr "cargue más transferencia luego de la última"
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
-msgid "hide"
-msgstr ""
+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/templates/create/CreatePage.tsx:218
+#: src/paths/instance/transfers/list/ListPage.tsx:76
#, c-format
-msgid "show"
-msgstr ""
+msgid "Bank account"
+msgstr "Cuenta bancaria"
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, fuzzy, c-format
-msgid "could not inform template"
-msgstr "no se pudo informar la transferencia"
+msgid "All accounts"
+msgstr "Cuenta"
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, fuzzy, c-format
-msgid "Amount is required"
-msgstr "Login necesario"
+msgid "Filter by account address"
+msgstr "filtrar por dirección de cuenta"
-#: src/paths/instance/templates/use/UsePage.tsx:58
-#, c-format
-msgid "Order summary is required"
-msgstr ""
+#: src/paths/instance/transfers/list/ListPage.tsx:105
+#, fuzzy, c-format
+msgid "Only show wire transfers confirmed by the merchant"
+msgstr "mostrar sólo las transferencias confirmadas por el comerciante"
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/transfers/list/ListPage.tsx:115
#, fuzzy, c-format
-msgid "New order for template"
-msgstr "cargar viejas transferencias"
+msgid "Only show wire transfers claimed by the exchange"
+msgstr "sólo muestran las transferencias reclamadas por el proveedor"
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
-msgid "Amount of the order"
-msgstr ""
+msgid "Unverified"
+msgstr "Sin verificar"
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/transfers/list/index.tsx:118
#, fuzzy, c-format
-msgid "Order summary"
-msgstr "Estado de orden"
+msgid "Wire transfer \"%1$s...\" has been deleted"
+msgstr "La instancia '%1$s' (ID: %2$s) fue eliminada"
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/transfers/list/index.tsx:123
#, fuzzy, c-format
-msgid "could not create order from template"
-msgstr "No se pudo create el reembolso"
+msgid "Failed to delete transfer"
+msgstr "Fallo al eliminar instancia"
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/admin/create/CreatePage.tsx:86
#, 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."
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
-#, fuzzy, c-format
-msgid "Fixed amount"
-msgstr "Monto reembolzado"
+#: src/paths/admin/create/CreatePage.tsx:104
+#, c-format
+msgid "Pay delay can't be greater than wire transfer delay"
+msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/admin/create/CreatePage.tsx:112
#, fuzzy, c-format
-msgid "Default amount"
-msgstr "Monto reembolzado"
+msgid "Max 7 lines"
+msgstr "máximo 7 líneas"
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/admin/create/CreatePage.tsx:138
+#, c-format
+msgid "Doesn't match"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:215
#, fuzzy, c-format
-msgid "Default summary"
-msgstr "Estado de orden"
+msgid "Enable access control"
+msgstr "Administrar token de acceso"
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "Print"
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "Setup TOTP"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "Templates"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "add new templates"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/admin/create/CreatePage.tsx:274
+#, 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/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "load more templates before the first one"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
+"Nombre de la instancia en URL. La instancia \"por defecto\" es especial, ya "
+"que se utiliza para administrar otras instancias."
-#: src/paths/instance/templates/list/Table.tsx:146
-#, fuzzy, c-format
-msgid "load newer templates"
-msgstr "cargar nuevas transferencias"
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Business name"
+msgstr "Nombre del negocio"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr "Nombre legal de la empresa representada por esta instancia."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
+#, c-format
+msgid "Email"
+msgstr "Correo eletrónico"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
+#, c-format
+msgid "Contact email"
+msgstr "Correo electrónico del contacto"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
+#, c-format
+msgid "Website URL"
+msgstr "URL de sitio web"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
+#, c-format
+msgid "URL."
+msgstr "URL."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
+#, c-format
+msgid "Logo"
+msgstr "Logotipo"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
+#, c-format
+msgid "Logo image."
+msgstr "Imagen del logotipo."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
+#, c-format
+msgid "Physical location of the merchant."
+msgstr "Ubicación física del comerciante."
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "delete selected templates from the database"
+msgid "Jurisdiction"
+msgstr "Jurisdicción"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr "Jurisdicción para disputas legales con el comerciante."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid "Pay transaction fee"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "use template to create new order"
+msgid "Assume the cost of the transaction of let the user pay for it."
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/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Default payment delay"
+msgstr "Retraso del pago por defecto"
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "load more templates after the last one"
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
+"Tiempo que los clientes tienen para pagar un pedido antes de que caduque la "
+"oferta de forma predeterminada."
-#: src/paths/instance/templates/list/Table.tsx:214
-#, fuzzy, c-format
-msgid "load older templates"
-msgstr "cargar viejas transferencias"
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Default wire transfer delay"
+msgstr "Retrazo de transferencia por omisión"
-#: 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/components/instance/DefaultInstanceFormFields.tsx:115
+#, 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 ""
+"Tiempo máximo que un proveedor puede retrasar la transferencia de fondos al "
+"comerciante, lo que le permite agrupar pagos más pequeños en transferencias "
+"más grandes y reducir las comisiones por transferencia."
-#: src/paths/instance/templates/list/index.tsx:104
-#, fuzzy, c-format
-msgid "template delete successfully"
-msgstr "producto fue eliminado correctamente"
+#: src/paths/instance/update/UpdatePage.tsx:124
+#, c-format
+msgid "Instance id"
+msgstr "ID de instancia"
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/update/index.tsx:108
#, fuzzy, c-format
-msgid "could not delete the template"
-msgstr "no se pudo eliminar el producto"
+msgid "Failed to update instance"
+msgstr "Fallo al crear la instancia"
-#: 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:54
+#, c-format
+msgid "Must be \"pay\" or \"refund\""
+msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, fuzzy, c-format
-msgid "should be one of '%1$s'"
+msgid "Must 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 ""
+msgstr "ID de webhook a utilizar"
#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
msgid "Event"
+msgstr "Evento"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
+#, c-format
+msgid "Pay"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
msgid "The event of the webhook: why the webhook is used"
-msgstr ""
+msgstr "El evento del webhook: por qué se utiliza el webhook"
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
msgid "Method"
-msgstr ""
+msgstr "Método"
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "Method used by the webhook"
+msgid "GET"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "URL"
-msgstr "URL"
+msgid "POST"
+msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "PUT"
msgstr ""
#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Header"
+msgid "PATCH"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid "Header template of the webhook"
+msgid "HEAD"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Body"
-msgstr ""
+msgid "Method used by the webhook"
+msgstr "Método utilizado por el webhook"
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
#, c-format
-msgid "Body template by the webhook"
+msgid "URL"
+msgstr "URL"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr "URL del webhook al que se redirigirá al cliente"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
+#, c-format
+msgid ""
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Webhooks"
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "add new webhooks"
+msgid "The short list of variables are:"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
+#, fuzzy, c-format
+msgid "order's description"
+msgstr "descripción"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
+#, fuzzy, c-format
+msgid "order's price"
+msgstr "Precio de la orden"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid "load more webhooks before the first one"
+msgid "order's unique identification"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, fuzzy, c-format
-msgid "load newer webhooks"
-msgstr "cargar nuevas ordenes"
+msgid "the amount that was being refunded"
+msgstr "monto a ser reembolsado"
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, c-format
-msgid "Event type"
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "delete selected webhook from the database"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Http body"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
+#, c-format
+msgid "Body template by the webhook"
+msgstr "Plantilla del cuerpo del webhook"
+
+#: src/paths/instance/webhooks/create/index.tsx:52
+#, fuzzy, c-format
+msgid "Webhook create successfully"
+msgstr "el webhook ha sido borrado correctamente"
+
+#: src/paths/instance/webhooks/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create the webhook"
+msgstr "no se ha podido eliminar el webhook"
+
+#: src/paths/instance/webhooks/create/index.tsx:66
+#, fuzzy, c-format
+msgid "Could not create webhook"
+msgstr "no se ha podido eliminar el webhook"
+
+#: src/paths/instance/webhooks/list/Table.tsx:57
+#, c-format
+msgid "Webhooks"
+msgstr "Webhooks"
+
+#: src/paths/instance/webhooks/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Add new webhooks"
+msgstr "añadir nuevos webhooks"
+
+#: src/paths/instance/webhooks/list/Table.tsx:117
+#, fuzzy, c-format
+msgid "Load more webhooks before the first one"
+msgstr "cargar más webhooks antes del primero"
+
+#: src/paths/instance/webhooks/list/Table.tsx:130
+#, c-format
+msgid "Event type"
+msgstr "Tipo de evento"
+
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, fuzzy, c-format
-msgid "load older webhooks"
-msgstr "cargar viejas ordenes"
+msgid "Delete selected webhook from the database"
+msgstr "eliminar el webhook seleccionado de la base de datos"
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, fuzzy, c-format
+msgid "Load more webhooks after the last one"
+msgstr "cargar más webhooks después del último"
+
+#: src/paths/instance/webhooks/list/Table.tsx:190
+#, c-format
msgid "There is no webhooks yet, add more pressing the + sign"
-msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+msgstr "No hay webhooks todavía, añade más pulsando sobre el símbolo +"
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/webhooks/list/index.tsx:88
#, fuzzy, c-format
-msgid "webhook delete successfully"
-msgstr "producto fue eliminado correctamente"
+msgid "Webhook delete successfully"
+msgstr "el webhook ha sido borrado correctamente"
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/webhooks/list/index.tsx:93
#, fuzzy, c-format
-msgid "could not delete the webhook"
-msgstr "no se pudo eliminar el producto"
+msgid "Could not delete the webhook"
+msgstr "no se ha podido eliminar el webhook"
+
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
+#, c-format
+msgid "Header"
+msgstr "Cabecera"
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
+#, c-format
+msgid "Header template of the webhook"
+msgstr "Plantilla de cabecera del webhook"
+
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
+#, c-format
+msgid "Body"
+msgstr "Cuerpo"
+
+#: src/paths/instance/webhooks/update/index.tsx:88
+#, fuzzy, c-format
+msgid "Webhook updated"
+msgstr "ID de webhook a utilizar"
+
+#: src/paths/instance/webhooks/update/index.tsx:94
#, fuzzy, c-format
-msgid "check the id, does not look valid"
-msgstr "verificar el id, no parece válido"
+msgid "Could not update webhook"
+msgstr "no se ha podido eliminar el webhook"
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "should have 52 characters, current %1$s"
-msgstr "debería tener 52 caracteres, actualmente %1$s"
+msgid "Language"
+msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "URL doesn't have the right format"
-msgstr "La URL no tiene el formato correcto"
+msgid "Set default"
+msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Credited bank account"
+msgid "Advance order creation"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/settings/index.tsx:103
#, c-format
-msgid "Select one account"
+msgid "Shows more options in the order creation form"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/settings/index.tsx:107
#, c-format
-msgid "Bank account of the merchant where the payment was received"
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/settings/index.tsx:113
#, fuzzy, c-format
-msgid "Wire transfer ID"
-msgstr "Id de transferencia"
+msgid "Date format"
+msgstr "formato inválido"
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/settings/index.tsx:131
#, c-format
-msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
-"characters long"
+msgid "How the date is going to be displayed"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/settings/index.tsx:134
#, c-format
-msgid ""
-"Base URL of the exchange that made the transfer, should have been in the "
-"wire transfer subject"
+msgid "Developer mode"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:117
+#: src/paths/settings/index.tsx:135
#, c-format
-msgid "Amount credited"
+msgid ""
+"Shows more options and tools which are not intended for general audience."
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/categories/list/Table.tsx:133
+#, fuzzy, c-format
+msgid "Total products"
+msgstr "Precio total"
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, fuzzy, c-format
+msgid "Delete selected category from the database"
+msgstr "eliminar transferencia seleccionada de la base de datos"
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, fuzzy, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr "No hay propinas todavía, agregar mas presionando el signo +"
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, fuzzy, c-format
+msgid "Category delete successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/categories/list/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not delete the category"
+msgstr "no se pudo eliminar el producto"
+
+#: src/paths/instance/categories/create/CreatePage.tsx:77
#, c-format
-msgid "Actual amount that was wired to the merchant's bank account"
+msgid "Category name"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/categories/create/index.tsx:53
+#, fuzzy, c-format
+msgid "Category added successfully"
+msgstr "producto fue eliminado correctamente"
+
+#: src/paths/instance/categories/create/index.tsx:59
+#, fuzzy, c-format
+msgid "Could not add category"
+msgstr "no se pudo crear el producto"
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
#, c-format
-msgid "could not inform transfer"
-msgstr "no se pudo informar la transferencia"
+msgid "Id:"
+msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, fuzzy, c-format
+msgid "Name of the category"
+msgstr "Nombre de la plantilla en las URL."
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
#, c-format
-msgid "Transfers"
-msgstr "Transferencias"
+msgid "Products"
+msgstr "Productos"
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
#, fuzzy, c-format
-msgid "add new transfer"
-msgstr "cargar nuevas transferencias"
+msgid "Search by product description or id"
+msgstr "buscar productos por su descripción o ID"
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
#, c-format
-msgid "load more transfers before the first one"
+msgid "Products that this category will list."
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
-#, c-format
-msgid "load newer transfers"
-msgstr "cargar nuevas transferencias"
+#: src/paths/instance/categories/update/index.tsx:93
+#, fuzzy, c-format
+msgid "Could not update category"
+msgstr "no se pudo actualizar el producto"
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/categories/update/index.tsx:95
#, c-format
-msgid "Credit"
-msgstr "Crédito"
+msgid "Category id is unknown"
+msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/Routing.tsx:659
#, c-format
-msgid "Confirmed"
-msgstr "Confirmado"
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/Routing.tsx:669
#, c-format
-msgid "Verified"
-msgstr "Verificado"
+msgid "Hide for today"
+msgstr "Ocultar por hoy"
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/Routing.tsx:703
+#, fuzzy, c-format
+msgid "KYC verification needed"
+msgstr "Verificación KYC pendiente"
+
+#: src/Routing.tsx:707
#, c-format
-msgid "Executed at"
-msgstr "Ejecutado en"
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
+msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/components/menu/SideBar.tsx:157
+#, fuzzy, c-format
+msgid "Configuration"
+msgstr "Expiración"
+
+#: src/components/menu/SideBar.tsx:196
#, c-format
-msgid "yes"
-msgstr "si"
+msgid "Settings"
+msgstr "Configuración"
+
+#: src/components/menu/SideBar.tsx:206
+#, fuzzy, c-format
+msgid "Access token"
+msgstr "Token de acceso"
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/components/menu/SideBar.tsx:214
#, c-format
-msgid "no"
-msgstr "no"
+msgid "Connection"
+msgstr "Conexión"
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/components/menu/SideBar.tsx:223
#, c-format
-msgid "unknown"
-msgstr "desconocido"
+msgid "Interface"
+msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/components/menu/SideBar.tsx:264
#, c-format
-msgid "delete selected transfer from the database"
-msgstr "eliminar transferencia seleccionada de la base de datos"
+msgid "List"
+msgstr "Lista"
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/components/menu/SideBar.tsx:283
#, c-format
-msgid "load more transfer after the last one"
-msgstr "cargue más transferencia luego de la última"
+msgid "Log out"
+msgstr "Salir"
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/admin/create/index.tsx:54
#, c-format
-msgid "load older transfers"
-msgstr "cargar viejas transferencias"
+msgid "Failed to create instance"
+msgstr "Fallo al crear la instancia"
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/Application.tsx:208
#, c-format
-msgid "There is no transfer yet, add more pressing the + sign"
-msgstr "No hay transferencias todavía, agregar mas presionando el signo +"
+msgid "checking compatibility with server..."
+msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/Application.tsx:217
#, fuzzy, c-format
-msgid "filter by account address"
-msgstr "Dirección de cuenta"
+msgid "Contacting the server failed"
+msgstr "No se pudo aceder al servidor"
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/Application.tsx:229
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "The server version is not supported"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/Application.tsx:230
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
-#, fuzzy, c-format
-msgid "Unverified"
-msgstr "Verificado"
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr "Borrando"
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/components/form/InputSecured.tsx:41
#, c-format
-msgid "is not valid"
-msgstr ""
+msgid "Changing"
+msgstr "Cambiando"
+
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr "Administrar token de acceso"
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
#, fuzzy, c-format
-msgid "is not a number"
-msgstr "Número de edificio"
+msgid "Business Name"
+msgstr "Nombre del negocio"
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
-msgid "must be 1 or greater"
-msgstr "debe ser 1 o mayor"
+msgid "Order ID"
+msgstr "ID de pedido"
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
-msgid "max 7 lines"
-msgstr "máximo 7 líneas"
+msgid "Payment URL"
+msgstr "URL de pago"
-#: src/paths/admin/create/CreatePage.tsx:178
#, c-format
-msgid "change authorization configuration"
-msgstr "cambiar configuración de autorización"
+#~ msgid "cannot be empty"
+#~ msgstr "no puede ser vacío"
-#: 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"
+#~ msgid "KYC URL"
+#~ msgstr "URL de KYC"
-#: 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."
+#~ msgid "clear"
+#~ msgstr "limpiar"
-#: 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."
+#~ msgid "Product"
+#~ msgstr "Producto"
-#: src/components/form/InputPaytoForm.tsx:118
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
-msgstr "Los números IBAN usualmente tienen mas de 4 digitos"
+#~ msgid "image"
+#~ msgstr "imagen"
-#: src/components/form/InputPaytoForm.tsx:120
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
-msgstr "Los números IBAN usualmente tienen menos de 34 digitos"
+#~ msgid "quantity"
+#~ msgstr "cantidad"
-#: src/components/form/InputPaytoForm.tsx:128
#, c-format
-msgid "IBAN country code not found"
-msgstr "Código de pais de IBAN no encontrado"
+#~ msgid "total price"
+#~ msgstr "precio total"
-#: src/components/form/InputPaytoForm.tsx:153
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
-msgstr "El número IBAN no es válido, falló la verificación"
+#~ msgid "not a valid json"
+#~ msgstr "no es un json válido"
-#: src/components/form/InputPaytoForm.tsx:248
#, c-format
-msgid "Target type"
-msgstr "Tipo objetivo"
+#~ msgid "Auto-refund deadline"
+#~ msgstr "Plazo de reembolso automático"
-#: src/components/form/InputPaytoForm.tsx:249
#, c-format
-msgid "Method to use for wire transfer"
-msgstr "Método a usar para la transferencia"
+#~ msgid "Maximum deposit fee"
+#~ msgstr "Máxima tarifa de depósito"
-#: src/components/form/InputPaytoForm.tsx:258
#, c-format
-msgid "Routing"
-msgstr "Enrutamiento"
+#~ 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 ""
+#~ "Máximo total de comisiones por transferencia que el vendedor está "
+#~ "dispuesto a cubrir para este pedido. Los gastos de transferencia que "
+#~ "superen este importe correrán a cargo del cliente."
-#: src/components/form/InputPaytoForm.tsx:259
#, c-format
-msgid "Routing number."
-msgstr "Número de enrutamiento."
+#~ msgid "Wire fee amortization"
+#~ msgstr "Amortización de comisión de transferencia"
-#: src/components/form/InputPaytoForm.tsx:263
#, c-format
-msgid "Account"
-msgstr "Cuenta"
+#~ 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 ""
+#~ "Factor por el que se dividen los comisiones por transferencia que superan "
+#~ "el umbral anterior para determinar la parte del exceso de comisiones por "
+#~ "transferencia que debe pagar explícitamente el consumidor."
-#: src/components/form/InputPaytoForm.tsx:264
-#, fuzzy, c-format
-msgid "Account number."
-msgstr "Dirección de cuenta"
+#, c-format
+#~ msgid ""
+#~ "Uncheck this option if the merchant backend generated an order ID with "
+#~ "enough entropy to prevent adversarial claims."
+#~ msgstr ""
+#~ "Desmarque esta opción si el backend del comerciante ha generado un ID de "
+#~ "pedido con suficiente entropía para evitar reclamaciones de adversarios."
-#: src/components/form/InputPaytoForm.tsx:273
#, c-format
-msgid "Business Identifier Code."
-msgstr ""
+#~ msgid "load newer orders"
+#~ msgstr "cargar nuevas ordenes"
-#: src/components/form/InputPaytoForm.tsx:282
#, c-format
-msgid "Bank Account Number."
-msgstr ""
+#~ msgid "load older orders"
+#~ msgstr "cargar viejas ordenes"
-#: src/components/form/InputPaytoForm.tsx:292
#, c-format
-msgid "Unified Payment Interface."
-msgstr "Interfaz de pago unificado."
+#~ msgid "date"
+#~ msgstr "fecha"
-#: src/components/form/InputPaytoForm.tsx:301
#, c-format
-msgid "Bitcoin protocol."
-msgstr ""
+#~ msgid "amount"
+#~ msgstr "monto"
-#: src/components/form/InputPaytoForm.tsx:310
#, c-format
-msgid "Ethereum protocol."
-msgstr ""
+#~ msgid "reason"
+#~ msgstr "razón"
-#: src/components/form/InputPaytoForm.tsx:319
#, c-format
-msgid "Interledger protocol."
-msgstr ""
+#~ msgid "Max wire fee"
+#~ msgstr "Impuesto de transferencia máximo"
-#: src/components/form/InputPaytoForm.tsx:328
#, c-format
-msgid "Host"
-msgstr ""
+#~ msgid "maximum wire fee accepted by the merchant"
+#~ msgstr "comisión máxima por transferencia aceptada por el comerciante"
-#: src/components/form/InputPaytoForm.tsx:329
#, c-format
-msgid "Bank host."
-msgstr ""
+#~ msgid ""
+#~ "over how many customer transactions does the merchant expect to amortize "
+#~ "wire fees on average"
+#~ msgstr ""
+#~ "en cuántas transacciones de clientes espera el comerciante amortizar los "
+#~ "gastos de transferencia por término medio"
-#: src/components/form/InputPaytoForm.tsx:334
#, c-format
-msgid "Bank account."
-msgstr ""
+#~ msgid "paid"
+#~ msgstr "pagados"
-#: src/components/form/InputPaytoForm.tsx:343
#, c-format
-msgid "Bank account owner's name."
-msgstr ""
+#~ msgid "refunded"
+#~ msgstr "reembolzado"
-#: src/components/form/InputPaytoForm.tsx:370
#, c-format
-msgid "No accounts yet."
-msgstr ""
+#~ msgid "refund"
+#~ msgstr "reembolzar"
-#: 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 ""
+#~ msgid "created at"
+#~ msgstr "creado"
-#: src/components/instance/DefaultInstanceFormFields.tsx:58
-#, fuzzy, c-format
-msgid "Business name"
-msgstr "Nombre de edificio"
+#, c-format
+#~ msgid "date (YYYY/MM/DD)"
+#~ msgstr "Fecha(AAAA/MM/DD)"
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Legal name of the business represented by this instance."
-msgstr ""
+#~ msgid "could not get the order to refund"
+#~ msgstr "no se ha podido obtener el reembolso para el pedido"
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
#, c-format
-msgid "Email"
-msgstr "Correo eletrónico"
+#~ msgid "Delivery address"
+#~ msgstr "Dirección de entrega"
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
#, c-format
-msgid "Contact email"
-msgstr ""
+#~ msgid "Sell"
+#~ msgstr "Venta"
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
#, c-format
-msgid "Website URL"
-msgstr "URL de sitio web"
+#~ msgid "Profit"
+#~ msgstr "Ganancia"
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
#, c-format
-msgid "URL."
-msgstr ""
+#~ msgid "free"
+#~ msgstr "Gratis"
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
#, c-format
-msgid "Logo"
-msgstr ""
+#~ 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 ""
+#~ "Para completar la configuración de la reserva, ahora debe iniciar una "
+#~ "transferencia bancaria utilizando el asunto de transferencia bancaria "
+#~ "indicado y abonando el importe especificado en la cuenta indicada del "
+#~ "proveedor."
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
#, c-format
-msgid "Logo image."
-msgstr ""
+#~ msgid ""
+#~ "If your system supports RFC 8905, you can do this by opening this URI:"
+#~ msgstr "Si su sistema soporta RFC 8905, puede hacerlo abriendo este URI:"
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
#, c-format
-msgid "Bank account"
-msgstr "Cuenta bancaria"
+#~ msgid "it should be greater than 0"
+#~ msgstr "debe ser mayor que 0"
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
#, c-format
-msgid "URI specifying bank account for crediting revenue."
-msgstr ""
+#~ msgid "must be a valid URL"
+#~ msgstr "debe ser una URL válida"
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
#, c-format
-msgid "Default max deposit fee"
-msgstr "Impuesto máximo de deposito por omisión"
+#~ msgid "Initial balance"
+#~ msgstr "Balance inicial"
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
-msgstr ""
+#~ msgid "balance prior to deposit"
+#~ msgstr "saldo antes del depósito"
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Default max wire fee"
-msgstr "Impuesto máximo de transferencia por omisión"
+#~ msgid "Next"
+#~ msgstr "Siguiente"
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
-msgstr ""
+#~ msgid "method to use for wire transfer"
+#~ msgstr "método a usar para realizar la transferencia bancaria"
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
#, c-format
-msgid "Default wire fee amortization"
-msgstr "Amortización de impuesto de transferencia por omisión"
+#~ msgid "Select one wire method"
+#~ msgstr "Selecciona un método de transferencia"
-#: 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 ""
+#~ msgid "Created balance"
+#~ msgstr "Balance creado"
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Physical location of the merchant."
-msgstr ""
+#~ msgid "Exchange balance"
+#~ msgstr "Balance del proveedor"
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "Jurisdiction"
-msgstr "Jurisdicción"
+#~ msgid "Picked up"
+#~ msgstr "Recogido"
-#: 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."
+#~ msgid "Committed"
+#~ msgstr "Comiteado"
+
+#, c-format
+#~ msgid "Subject"
+#~ msgstr "Asunto"
+
+#, c-format
+#~ msgid "Tips"
+#~ msgstr "Propinas"
+
+#, c-format
+#~ msgid "No tips has been authorized from this reserve"
+#~ msgstr "No se han autorizado propinas de esta reserva"
+
+#, c-format
+#~ msgid "Authorized"
+#~ msgstr "Autorizado"
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
#, fuzzy, c-format
-msgid "Default payment delay"
-msgstr "Retrazo de pago por omisión"
+#~ msgid "amount of tip"
+#~ msgstr "monto"
+
+#, fuzzy, c-format
+#~ msgid "Justification"
+#~ msgstr "Jurisdicción"
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
-msgstr ""
+#~ msgid "reason for the tip"
+#~ msgstr "motivo de la propina"
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
#, c-format
-msgid "Default wire transfer delay"
-msgstr "Retrazo de transferencia por omisión"
+#~ msgid "URL after tip"
+#~ msgstr "URL después de la recompensa"
-#: 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 ""
+#~ msgid "URL to visit after tip payment"
+#~ msgstr "URL para visitar después del pago de la propina"
+
+#, fuzzy, c-format
+#~ msgid "Reserves not yet funded"
+#~ msgstr "Servidor no encontrado"
-#: src/paths/instance/update/UpdatePage.tsx:164
#, c-format
-msgid "Instance id"
-msgstr "ID de instancia"
+#~ msgid "Reserves ready"
+#~ msgstr "Reservas listas"
+
+#, c-format
+#~ msgid "Expires at"
+#~ msgstr "Vence en"
+
+#, c-format
+#~ msgid "Initial"
+#~ msgstr "Inicial"
+
+#, c-format
+#~ msgid "authorize new tip from selected reserve"
+#~ msgstr "autorizar nueva punta de reserva seleccionada"
-#: 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"
+#~ 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 +"
+
+#, fuzzy, c-format
+#~ msgid "Expected Balance"
+#~ msgstr "Ejecutado en"
+
+#, fuzzy, c-format
+#~ msgid "should not be empty"
+#~ msgstr "no puede ser vacío"
+
+#, fuzzy, c-format
+#~ msgid "should be greater that 0"
+#~ msgstr "Debe ser mayor a 0"
+
+#, fuzzy, c-format
+#~ msgid "Fixed summary"
+#~ msgstr "Estado de orden"
+
+#, fuzzy, c-format
+#~ msgid "Fixed price"
+#~ msgstr "precio unitario"
-#: src/paths/instance/update/UpdatePage.tsx:182
#, c-format
-msgid "Manage access token"
-msgstr "Administrar token de acceso"
+#~ msgid "Point-of-sale key"
+#~ msgstr "Clave punto de venta"
-#: src/paths/instance/update/index.tsx:112
#, c-format
-msgid "Failed to create instance"
-msgstr "Fallo al crear la instancia"
+#~ msgid "Useful to validate the purchase"
+#~ msgstr "Útil para validar la compra"
-#: src/components/exception/login.tsx:74
#, c-format
-msgid "Login required"
-msgstr "Login necesario"
+#~ msgid "show secret key"
+#~ msgstr "mostrar clave secreta"
-#: src/components/exception/login.tsx:80
#, c-format
-msgid "Please enter your access token."
-msgstr ""
+#~ msgid "hide secret key"
+#~ msgstr "ocultar clave secreta"
+
+#, c-format
+#~ msgid "hide"
+#~ msgstr "ocultar"
+
+#, c-format
+#~ msgid "show"
+#~ msgstr "mostrar"
-#: src/components/exception/login.tsx:108
#, fuzzy, c-format
-msgid "Access Token"
-msgstr "Acceso denegado"
+#~ msgid "could not inform template"
+#~ msgstr "no se pudo informar la transferencia"
-#: src/InstanceRoutes.tsx:171
#, c-format
-msgid "The request to the backend take too long and was cancelled"
-msgstr ""
+#~ 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 ""
+#~ "Aquí puede especificar un valor por defecto para los campos que no son "
+#~ "fijos. Los valores por defecto pueden ser editados por el cliente antes "
+#~ "del pago."
+
+#, fuzzy, c-format
+#~ msgid "Default summary"
+#~ msgstr "Estado de orden"
-#: src/InstanceRoutes.tsx:172
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
-msgstr ""
+#~ msgid "Setup TOTP"
+#~ msgstr "Configurar TOTP"
-#: 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"
+#~ msgid "load older templates"
+#~ msgstr "cargar viejas transferencias"
+
+#, fuzzy, c-format
+#~ msgid "load newer webhooks"
+#~ msgstr "cargar nuevas ordenes"
-#: src/InstanceRoutes.tsx:179
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
-msgstr ""
+#~ msgid "load older webhooks"
+#~ msgstr "cargar webhooks antiguos"
-#: src/InstanceRoutes.tsx:196
#, c-format
-msgid "Access denied"
-msgstr "Acceso denegado"
+#~ msgid "load newer transfers"
+#~ msgstr "cargar nuevas transferencias"
-#: src/InstanceRoutes.tsx:197
#, c-format
-msgid "The access token provided is invalid."
-msgstr ""
+#~ msgid "load older transfers"
+#~ msgstr "cargar viejas transferencias"
-#: src/InstanceRoutes.tsx:212
-#, fuzzy, c-format
-msgid "No 'default' instance configured yet."
-msgstr "Sin instancia default"
+#, c-format
+#~ msgid "is not valid"
+#~ msgstr "no es válido"
-#: src/InstanceRoutes.tsx:213
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
-msgstr ""
+#~ msgid "must be 1 or greater"
+#~ msgstr "debe ser 1 o mayor"
-#: src/InstanceRoutes.tsx:630
#, c-format
-msgid "The access token provided is invalid"
-msgstr ""
+#~ msgid "change authorization configuration"
+#~ msgstr "cambiar configuración de autorización"
-#: src/InstanceRoutes.tsx:664
#, c-format
-msgid "Hide for today"
-msgstr ""
+#~ msgid "Target type"
+#~ msgstr "Tipo objetivo"
-#: src/components/menu/SideBar.tsx:82
#, c-format
-msgid "Instance"
-msgstr "Instancia"
+#~ msgid "Bank account owner's name."
+#~ msgstr "Nombre del titular de la cuenta bancaria."
-#: src/components/menu/SideBar.tsx:91
#, c-format
-msgid "Settings"
-msgstr "Configuración"
+#~ msgid "No accounts yet."
+#~ msgstr "Aún no hay cuentas."
-#: src/components/menu/SideBar.tsx:167
#, c-format
-msgid "Connection"
-msgstr "Conexión"
+#~ msgid "Default max deposit fee"
+#~ msgstr "Impuesto máximo de deposito por omisión"
-#: src/components/menu/SideBar.tsx:209
#, c-format
-msgid "New"
-msgstr "Nuevo"
+#~ msgid ""
+#~ "Maximum deposit fees this merchant is willing to pay per order by default."
+#~ msgstr ""
+#~ "Comisiones de depósito máximas que este comerciante está dispuesto a "
+#~ "pagar por pedido por defecto."
-#: src/components/menu/SideBar.tsx:219
#, c-format
-msgid "List"
-msgstr "Lista"
+#~ msgid "Default max wire fee"
+#~ msgstr "Impuesto máximo de transferencia por omisión"
-#: src/components/menu/SideBar.tsx:234
#, c-format
-msgid "Log out"
-msgstr "Salir"
+#~ msgid ""
+#~ "Maximum wire fees this merchant is willing to pay per wire transfer by "
+#~ "default."
+#~ msgstr ""
+#~ "Comisiones de transferencia máximas que este comerciante está dispuesto a "
+#~ "pagar por transferencia por defecto."
-#: src/ApplicationReadyRoutes.tsx:71
#, c-format
-msgid "Check your token is valid"
-msgstr "Verifica que el token sea valido"
+#~ msgid "Default wire fee amortization"
+#~ msgstr "Amortización de impuesto de transferencia por omisión"
-#: src/ApplicationReadyRoutes.tsx:90
#, c-format
-msgid "Couldn't access the server."
-msgstr "No se pudo acceder al servidor."
+#~ msgid ""
+#~ "Number of orders excess wire transfer fees will be divided by to compute "
+#~ "per order surcharge."
+#~ msgstr ""
+#~ "El número de pedidos que excedan las tarifas de transferencia bancaria se "
+#~ "dividirá para calcular el recargo por pedido."
-#: 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"
+#~ msgid "Change the authorization method use for this instance."
+#~ msgstr "Cambiar el método de autorización a usar para esta instancia."
-#: src/Application.tsx:104
#, c-format
-msgid "Server not found"
-msgstr "Servidor no encontrado"
+#~ msgid "The request to the backend take too long and was cancelled"
+#~ msgstr "La petición al backend tardó demasiado y fue cancelada"
-#: src/Application.tsx:118
#, c-format
-msgid "Server response with an error code"
-msgstr ""
+#~ msgid "Diagnostic from %1$s is \"%2$s\""
+#~ msgstr "El Diagnóstico de %1$s es \"%2$s\""
-#: src/Application.tsx:120
#, c-format
-msgid "Got message %1$s from %2$s"
-msgstr "Recibimos el mensaje %1$s desde %2$s"
+#~ msgid "The backend reported a problem: HTTP status #%1$s"
+#~ msgstr "El backend ha informado de un problema: HTTP status #%1$s"
-#: src/Application.tsx:131
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
-msgstr ""
+#~ msgid "Diagnostic from %1$s is '%2$s'"
+#~ msgstr "El Diagnóstico de %1$s es '%2$s'"
-#: src/Application.tsx:144
#, c-format
-msgid "Unexpected Error"
-msgstr "Error inesperado"
+#~ msgid "Access denied"
+#~ msgstr "Acceso denegado"
-#: 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"
+#~ msgid "The access token provided is invalid."
+#~ msgstr "El token de acceso proporcionado no es válido."
-#: src/components/form/InputArray.tsx:110
#, c-format
-msgid "add element to the list"
-msgstr "agregar elemento a la lista"
+#~ msgid "The access token provided is invalid"
+#~ msgstr "El token de acceso proporcionado no es válido"
-#: src/components/form/InputArray.tsx:112
#, c-format
-msgid "add"
-msgstr "Agregar"
+#~ msgid "Instance"
+#~ msgstr "Instancia"
-#: src/components/form/InputSecured.tsx:37
#, c-format
-msgid "Deleting"
-msgstr "Borrando"
+#~ msgid "Check your token is valid"
+#~ msgstr "Verifica que el token sea valido"
-#: src/components/form/InputSecured.tsx:41
#, c-format
-msgid "Changing"
-msgstr "Cambiando"
+#~ msgid "Couldn't access the server."
+#~ msgstr "No se pudo acceder al servidor."
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
#, c-format
-msgid "Order ID"
-msgstr "ID de pedido"
+#~ 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/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
#, c-format
-msgid "Payment URL"
-msgstr "URL de pago"
+#~ msgid "Server not found"
+#~ msgstr "Servidor no encontrado"
+
+#, c-format
+#~ msgid "Got message %1$s from %2$s"
+#~ msgstr "Recibimos el mensaje %1$s desde %2$s"
#, c-format
-#~ msgid "Couldn't access the server"
-#~ msgstr "No se pudo aceder al servidor"
+#~ msgid "Response from server is unreadable, http status: %1$s"
+#~ msgstr "La respuesta del servidor es ilegible, estado http: %1$s"
+
+#, 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"
+
+#, c-format
+#~ msgid "add"
+#~ msgstr "Agregar"
#, c-format
#~ msgid "HTTP status #%1$s: Server reported a problem"
@@ -2814,10 +4190,6 @@ msgstr "URL de pago"
#~ msgstr "este producto no tiene impuestos"
#, c-format
-#~ msgid "Inventory products"
-#~ msgstr "Productos de inventario"
-
-#, c-format
#~ msgid "Total tax"
#~ msgstr "Impuesto total"
diff --git a/packages/merchant-backoffice-ui/src/i18n/fr.po b/packages/merchant-backoffice-ui/src/i18n/fr.po
index 4da5c5b59..b1b61b312 100644
--- a/packages/merchant-backoffice-ui/src/i18n/fr.po
+++ b/packages/merchant-backoffice-ui/src/i18n/fr.po
@@ -19,8 +19,8 @@ msgstr ""
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2024-02-28 08:07+0000\n"
"Last-Translator: d0p1 <contact@d0p1.eu>\n"
-"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
-"merchant-backoffice/fr/>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/merchant-"
+"backoffice/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -28,223 +28,815 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
"X-Generator: Weblate 5.2.1\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
+#, c-format
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
+msgstr ""
+
+#: src/components/modal/index.tsx:79
#, c-format
msgid "Cancel"
msgstr "Annuler"
-#: src/components/modal/index.tsx:79
+#: src/components/modal/index.tsx:87
#, c-format
msgid "%1$s"
msgstr ""
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr "Fermer"
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr "Continuer"
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr ""
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr "Confirmer"
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
#, c-format
-msgid "is not the same as the current access token"
+msgid "Required"
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:248
#, c-format
-msgid "cannot be empty"
-msgstr "ne peux pas être vide"
+msgid "Letter must be a JSON string"
+msgstr ""
+
+#: src/components/modal/index.tsx:250
+#, c-format
+msgid "JSON string is invalid"
+msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:255
#, c-format
-msgid "cannot be the same as the old token"
+msgid "Import"
msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:256
#, c-format
-msgid "is not the same"
+msgid "Importing an account from the bank"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr ""
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr ""
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr ""
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, c-format
+msgid "Account id"
+msgstr ""
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, c-format
+msgid "Is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:542
+#, c-format
+msgid "Can't be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:546
+#, c-format
+msgid "Is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:554
#, c-format
msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "access token currently in use"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, c-format
msgid "New access token"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:578
#, c-format
-msgid "next access token to be used"
+msgid "Next access token to be used"
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, c-format
msgid "Repeat access token"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:584
#, c-format
-msgid "confirm the same access token"
+msgid "Confirm the same access token"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, c-format
msgid "Clearing the access token will mean public access to the instance"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:616
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Can't be the same as the old access token"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
"backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, c-format
+msgid "Your instance not found"
+msgstr ""
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/paths/login/index.tsx:95
+#, c-format
+msgid "Please enter your access token for %1$s."
+msgstr ""
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/admin/list/TableActive.tsx:100
#, c-format
-msgid "add new instance"
+msgid "Add new instance"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, c-format
+msgid "This is not a valid host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, c-format
+msgid "Invalid url"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, c-format
+msgid "Account:"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, c-format
+msgid "Do not change"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -267,57 +859,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -360,66 +1002,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "Product"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "search products by it's description or id"
+msgid "no match found with that 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
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, c-format
+msgid "Search product"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:113
#, c-format
-msgid "how many products will be added"
+msgid "How many products will be added"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -432,54 +1069,74 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, c-format
+msgid "Invalid"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr ""
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -487,12 +1144,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -502,7 +1159,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -512,2213 +1169,2399 @@ 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"
+msgid "Amount in the current currency."
msgstr ""
-#: src/components/product/ProductList.tsx:41
+#: src/components/product/NonInventoryProductForm.tsx:208
#, c-format
-msgid "description"
+msgid "How many products will be added."
msgstr ""
-#: src/components/product/ProductList.tsx:44
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "quantity"
+msgid "Taxes"
msgstr ""
-#: src/components/product/ProductList.tsx:47
+#: src/components/product/ProductList.tsx:46
#, c-format
-msgid "unit price"
+msgid "Unit price"
msgstr ""
-#: src/components/product/ProductList.tsx:50
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "total price"
+msgid "Total price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:153
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "required"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "not valid"
+msgid "Refund deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "must be greater than 0"
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
+#: src/paths/instance/orders/create/CreatePage.tsx:188
#, c-format
-msgid "not a valid json"
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:170
+#: src/paths/instance/orders/create/CreatePage.tsx:196
#, c-format
-msgid "should be in the future"
+msgid "Must have a refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:173
+#: src/paths/instance/orders/create/CreatePage.tsx:201
#, c-format
-msgid "refund deadline cannot be before pay deadline"
+msgid "Auto refund can't be after refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:179
+#: src/paths/instance/orders/create/CreatePage.tsx:208
#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+msgid "Must be in the future"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:190
+#: src/paths/instance/orders/create/CreatePage.tsx:376
#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+msgid "Simple"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:197
+#: src/paths/instance/orders/create/CreatePage.tsx:388
#, c-format
-msgid "should have a refund deadline"
+msgid "Advanced"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:202
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
-msgid "auto refund cannot be after refund deadline"
+msgid "Manage products in order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:360
+#: src/paths/instance/orders/create/CreatePage.tsx:404
#, c-format
-msgid "Manage products in order"
+msgid "%1$s products with a total price of %2$s."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:437
+#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
-msgid "final order price"
+msgid "Final order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "address where the products will be delivered"
+msgid "Address where the products will be delivered"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, c-format
-msgid "Payment deadline"
+msgid "Payment time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, 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."
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:491
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, 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."
+msgid "Default"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:561
#, c-format
-msgid "Auto-refund deadline"
+msgid "Refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
msgid ""
-"Time until which the wallet will automatically check for refunds without "
-"user interaction."
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:502
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Maximum deposit fee"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, 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."
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:507
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Maximum wire fee"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Wire fee amortization"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid "days"
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:65
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, c-format
-msgid "hours"
+msgid "Additional information"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
-msgid "minutes"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "seconds"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "forever"
+msgid "Custom field name"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "%1$sM"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/create/CreatePage.tsx:796
#, c-format
-msgid "%1$sY"
+msgid "No deadline"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sd"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "%1$sh"
+msgid "Could not create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:70
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$smin"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:72
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$ssec"
+msgid "No more stock for product with id \"%1$s\"."
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:75
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "create order"
+msgid "Create order"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
+msgid "Load first page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, 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
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "No orders have been found matching your query!"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:288
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "duplicated"
+msgid "Load next page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:299
+#: src/paths/instance/orders/list/Table.tsx:233
#, 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"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "amount"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "reason"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "amount to be refunded"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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
+#: src/paths/instance/orders/list/Table.tsx:391
#, c-format
-msgid "requested by the customer"
+msgid "Requested by the customer"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "other"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "why this order is being refunded"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "more information to give context"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "total price for the transaction"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "Max wire fee"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize "
-"wire fees on average"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Created at"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "time when this contract was generated"
+msgid "Payment deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:116
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:122
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, c-format
-msgid "where the order will be delivered"
+msgid "Where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "claimed"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "claimed at"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/details/DetailPage.tsx:465
#, c-format
-msgid "wired"
+msgid "Wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/details/DetailPage.tsx:490
#, c-format
-msgid "refund order"
+msgid "Refund order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "not refundable"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, 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
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "pay at"
+msgid "Unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:666
+#: src/paths/instance/orders/details/DetailPage.tsx:659
#, c-format
-msgid "created at"
+msgid "Pay at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:707
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/orders/details/DetailPage.tsx:716
#, c-format
msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
msgid "Back"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/paths/instance/orders/details/index.tsx:88
#, c-format
-msgid "refund created successfully"
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "could not create the refund"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "select date to show nearby orders"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "order id"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "remove all filters"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "only show paid orders"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "Paid"
-msgstr ""
-
-#: src/paths/instance/orders/list/ListPage.tsx:142
-#, c-format
-msgid "only show orders with refunds"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "Refunded"
+msgid "Only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:152
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
msgid ""
-"only show orders where customers paid, but wire payments from payment "
+"Only show orders where customers paid, but wire payments from payment "
"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "clear date filter"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "Enter an order id"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "order not found"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "could not get the order to refund"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/orders/list/index.tsx:114
#, c-format
-msgid "Loading..."
+msgid "Order id"
msgstr ""
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid ""
-"click here to configure the stock of the product, leave it as is and the "
-"backend will not control stock"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/components/form/InputStock.tsx:109
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Manage stock"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/components/form/InputStock.tsx:115
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "this product has been configured without stock control"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/components/form/InputStock.tsx:119
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "Infinite"
+msgid "Internal id on the system"
msgstr ""
-#: src/components/form/InputStock.tsx:136
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Incoming"
+msgid "Verification algorithm"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Lost"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Current"
+msgid "Device key"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid "remove stock control for this product"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "without stock"
+msgid "Your device need to have exactly the same value"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "Next restock"
+msgid "Generate random secret key"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "Delivery address"
+msgid "Random"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "illustration of the product for customers"
+msgid "Device added successfully"
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "product description for customers"
+msgid "Could not add device"
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "Age restricted"
+msgid "OTP Devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/paths/instance/otp_devices/list/Table.tsx:62
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Add new devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid ""
-"sale price for customers, including taxes, for above units of the product"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "Stock"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid ""
-"product inventory for products with finite supply (for internal use only)"
+msgid "There is no devices yet, add more pressing the + sign"
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Need to complete marked fields"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "could not create product"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "Products"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:73
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "add product to inventory"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "Sell"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "Profit"
+msgid "Template id is unknown"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Sold"
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "free"
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "go to product update page"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "Update"
+msgid "This product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "remove this product from the database"
+msgid "Infinite"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "update the product with new price"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/components/form/InputStock.tsx:169
#, c-format
-msgid "update product with new price"
+msgid "Incoming"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/components/form/InputStock.tsx:170
#, c-format
-msgid "add more elements to the inventory"
+msgid "Lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/components/form/InputStock.tsx:185
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Current"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "new price for the product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/components/form/InputStock.tsx:195
#, c-format
-msgid "the are value with errors"
+msgid "without stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/components/form/InputStock.tsx:204
#, c-format
-msgid "update product with new stock and price"
+msgid "Next restock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "There is no products yet, add more pressing the + sign"
+msgid "Warehouse address"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:86
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product updated successfully"
+msgid "Add element to the list"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/components/product/ProductForm.tsx:120
#, c-format
-msgid "could not update the product"
+msgid "Invalid amount"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "product delete successfully"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/paths/instance/products/list/index.tsx:109
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "could not delete the product"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Product id:"
+msgid "Product description for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/components/product/ProductForm.tsx:197
#, 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."
+msgid "Age restriction"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/components/product/ProductForm.tsx:198
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Is this product restricted for customer below certain age?"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/components/product/ProductForm.tsx:199
#, c-format
-msgid "it should be greater than 0"
+msgid "Minimum age of the customer"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/components/product/ProductForm.tsx:203
#, c-format
-msgid "must be a valid URL"
+msgid "Unit name"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/components/product/ProductForm.tsx:204
#, c-format
-msgid "Initial balance"
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "balance prior to deposit"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid "Exchange URL"
+msgid "Price per unit"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/components/product/ProductForm.tsx:210
#, c-format
-msgid "URL of exchange"
+msgid ""
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/components/product/ProductForm.tsx:214
#, c-format
-msgid "Next"
+msgid "Stock"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid "Wire method"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "method to use for wire transfer"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Select one wire method"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/components/product/ProductForm.tsx:231
#, c-format
-msgid "could not create reserve"
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/components/product/ProductForm.tsx:232
#, c-format
-msgid "Valid until"
+msgid "Categories where this product will be listed on."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "Created balance"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Exchange balance"
+msgid "Could not create product"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "Picked up"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Committed"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Account address"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
-msgid "Subject"
+msgid "Sold"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "Tips"
+msgid "Free"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
-msgid "Authorized"
+msgid "Update"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/products/list/Table.tsx:283
#, c-format
-msgid "Expiration"
+msgid "Remove this product from the database"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "amount of tip"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "Justification"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/products/list/Table.tsx:373
#, c-format
-msgid "reason for the tip"
+msgid "Update product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/products/list/Table.tsx:384
+#, fuzzy, c-format
+msgid "Confirm update"
+msgstr "Confirmer"
+
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "URL after tip"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "Reserves not yet funded"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "Reserves ready"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "add new reserve"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "Expires at"
+msgid "There is no products yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
-msgid "Initial"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Product updated successfully"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid ""
-"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "Expected Balance"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/products/list/index.tsx:149
#, c-format
-msgid "could not create the tip"
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "should not be empty"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "should be greater that 0"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/products/update/UpdatePage.tsx:56
#, c-format
-msgid "can't be empty"
+msgid "Product id:"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/products/update/index.tsx:85
#, c-format
-msgid "to short"
+msgid "Product (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/create/CreatePage.tsx:96
#, c-format
-msgid "size of the key should be 32"
+msgid "Invalid. only characters and numbers"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/create/CreatePage.tsx:112
+#, c-format
+msgid "Must be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:119
+#, c-format
+msgid "To short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
msgid "Identifier"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Fixed summary"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Fixed price"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, c-format
+msgid "Amount is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
msgid "Minimum age"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, 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
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Verification algorithm"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "Point-of-sale key"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Useful to validate the purchase"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "generate random secret key"
+msgid "No device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "random"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "show secret key"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "hide secret key"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "hide"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/paths/instance/templates/list/Table.tsx:66
#, c-format
-msgid "show"
+msgid "Add new templates"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "could not inform template"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "Amount is required"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid "Order summary is required"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "New order for template"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "Amount of the order"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "Order summary"
+msgid "Load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/templates/list/index.tsx:91
#, c-format
-msgid "could not create order from template"
+msgid "Jump to template with the given template ID"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/instance/templates/list/index.tsx:92
#, 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."
+msgid "Template identification"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/paths/instance/templates/list/index.tsx:132
#, c-format
-msgid "Fixed amount"
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/templates/list/index.tsx:137
#, c-format
-msgid "Default amount"
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Default summary"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/templates/list/index.tsx:160
+#, c-format
+msgid "Deleting an template"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:162
+#, fuzzy, c-format
+msgid "can't be undone"
+msgstr "ne peux pas être vide"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
msgid "Print"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Setup TOTP"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "Templates"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "add new templates"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "load more templates before the first one"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "load newer templates"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "delete selected templates from the database"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "use template to create new order"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid "create qr code for the template"
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "load more templates after the last one"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "load older templates"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/token/DetailPage.tsx:121
#, c-format
-msgid "could not delete the template"
+msgid "Current access token"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "could not update template"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/token/DetailPage.tsx:142
#, c-format
-msgid "should be one of '%1$s'"
+msgid "Clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/token/DetailPage.tsx:177
+#, fuzzy, c-format
+msgid "Confirm change"
+msgstr "Confirmer"
+
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "Webhook ID to use"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "Event"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "Method"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "Method used by the webhook"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Header"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "Header template of the webhook"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Body"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Body template by the webhook"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "Webhooks"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
#, c-format
-msgid "add new webhooks"
+msgid "Duration"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "load newer webhooks"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "Event type"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "delete selected webhook from the database"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "load older webhooks"
+msgid "Go to token family update page"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "Remove this token family from the database"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
+#, c-format
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
+#, c-format
+msgid "Token family updated successfully"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
+#, c-format
+msgid "Could not update the token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, c-format
+msgid "Failed to delete token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
+#, c-format
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
#, c-format
-msgid "webhook delete successfully"
+msgid "Token familty updated successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
#, c-format
-msgid "could not delete the webhook"
+msgid "Could not update token family"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
#, c-format
-msgid "check the id, does not look valid"
+msgid "Check the id, does not look valid"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Must have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
#, 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
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
msgid "Amount credited"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "could not inform transfer"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Transfers"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "add new transfer"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/transfers/list/Table.tsx:64
#, c-format
-msgid "load more transfers before the first one"
+msgid "Add new transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "load newer transfers"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "unknown"
+msgid "never"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "delete selected transfer from the database"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "load more transfer after the last one"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "load older transfers"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/paths/instance/transfers/list/ListPage.tsx:76
#, c-format
-msgid "filter by account address"
+msgid "Bank account"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "All accounts"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/paths/instance/transfers/list/ListPage.tsx:105
+#, c-format
+msgid "Only show wire transfers confirmed by the merchant"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:115
+#, c-format
+msgid "Only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
msgid "Unverified"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid "is not valid"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid "is not a number"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "must be 1 or greater"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "max 7 lines"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "change authorization configuration"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+msgid "Doesn't match"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:82
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Enable access control"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "IBAN country code not found"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Target type"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Business name"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "Routing"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "Routing number."
+msgid "Email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "Account"
+msgid "Contact email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "Account number."
+msgid "Website URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "Business Identifier Code."
+msgid "URL."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "Bank Account Number."
+msgid "Logo"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "Unified Payment Interface."
+msgid "Logo image."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "Bitcoin protocol."
+msgid "Physical location of the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "Ethereum protocol."
+msgid "Jurisdiction"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Interledger protocol."
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "Host"
+msgid "Pay transaction fee"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "Bank host."
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Bank account."
+msgid "Default payment delay"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "Bank account owner's name."
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "No accounts yet."
+msgid "Default wire transfer delay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it "
-"is used to administer other instances."
+"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/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "Business name"
+msgid "Instance id"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/update/index.tsx:108
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Failed to update instance"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "Email"
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "Contact email"
+msgid "Must be one of '%1$s'"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "Website URL"
+msgid "Webhook ID to use"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "URL."
+msgid "Event"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "Logo"
+msgid "Pay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Logo image."
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Bank account"
+msgid "Method"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "GET"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Default max deposit fee"
+msgid "POST"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "PUT"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Default max wire fee"
+msgid "PATCH"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
+msgid "HEAD"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Default wire fee amortization"
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Physical location of the merchant."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Jurisdiction"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "order's description"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
#, c-format
-msgid "Default payment delay"
+msgid "order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
+msgid "order's unique identification"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Default wire transfer delay"
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, 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."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "Instance id"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Http body"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Manage access token"
+msgid "Body template by the webhook"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Failed to create instance"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Login required"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Please enter your access token."
+msgid "Could not create webhook"
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "Access Token"
+msgid "Webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "Add new webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Event type"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Access denied"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid "The access token provided is invalid."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Webhook delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "The access token provided is invalid"
+msgid "Header"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid "Hide for today"
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Instance"
+msgid "Body"
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Settings"
+msgid "Webhook updated"
msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Connection"
+msgid "Could not update webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "New"
+msgid "Language"
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "List"
+msgid "Set default"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Log out"
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, c-format
+msgid "Date format"
+msgstr ""
+
+#: src/paths/settings/index.tsx:131
+#, c-format
+msgid "How the date is going to be displayed"
+msgstr ""
+
+#: src/paths/settings/index.tsx:134
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/paths/settings/index.tsx:135
+#, c-format
+msgid ""
+"Shows more options and tools which are not intended for general audience."
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:133
+#, c-format
+msgid "Total products"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, c-format
+msgid "Delete selected category from the database"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, c-format
+msgid "Category delete successfully"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/paths/instance/categories/list/index.tsx:95
#, c-format
-msgid "Check your token is valid"
+msgid "Could not delete the category"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/paths/instance/categories/create/CreatePage.tsx:77
#, c-format
-msgid "Couldn't access the server."
+msgid "Category name"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/paths/instance/categories/create/index.tsx:53
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Category added successfully"
msgstr ""
-#: src/Application.tsx:104
+#: src/paths/instance/categories/create/index.tsx:59
#, c-format
-msgid "Server not found"
+msgid "Could not add category"
msgstr ""
-#: src/Application.tsx:118
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
#, c-format
-msgid "Server response with an error code"
+msgid "Id:"
msgstr ""
-#: src/Application.tsx:120
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "Name of the category"
msgstr ""
-#: src/Application.tsx:131
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, c-format
+msgid "Search by product description or id"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
+#, c-format
+msgid "Products that this category will list."
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:93
+#, c-format
+msgid "Could not update category"
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:95
+#, c-format
+msgid "Category id is unknown"
+msgstr ""
+
+#: src/Routing.tsx:659
+#, c-format
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/Routing.tsx:703
+#, c-format
+msgid "KYC verification needed"
+msgstr ""
+
+#: src/Routing.tsx:707
+#, c-format
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:157
+#, fuzzy, c-format
+msgid "Configuration"
+msgstr "Confirmer"
+
+#: src/components/menu/SideBar.tsx:196
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:206
+#, c-format
+msgid "Access token"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:214
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:223
+#, c-format
+msgid "Interface"
msgstr ""
-#: src/Application.tsx:144
+#: src/components/menu/SideBar.tsx:264
#, c-format
-msgid "Unexpected Error"
+msgid "List"
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/components/menu/SideBar.tsx:283
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Log out"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/paths/admin/create/index.tsx:54
#, c-format
-msgid "add element to the list"
+msgid "Failed to create instance"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/Application.tsx:208
#, c-format
-msgid "add"
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, c-format
+msgid "Contacting the server failed"
+msgstr ""
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2731,12 +3574,22 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/it.po b/packages/merchant-backoffice-ui/src/i18n/it.po
index 4055af10e..6af3b2291 100644
--- a/packages/merchant-backoffice-ui/src/i18n/it.po
+++ b/packages/merchant-backoffice-ui/src/i18n/it.po
@@ -28,223 +28,815 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.13.1\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
#, c-format
-msgid "Cancel"
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
msgstr ""
#: src/components/modal/index.tsx:79
#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:87
+#, c-format
msgid "%1$s"
msgstr ""
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr ""
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr ""
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr ""
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
+#, c-format
+msgid "Required"
+msgstr ""
+
+#: src/components/modal/index.tsx:248
+#, c-format
+msgid "Letter must be a JSON string"
+msgstr ""
+
+#: src/components/modal/index.tsx:250
+#, c-format
+msgid "JSON string is invalid"
+msgstr ""
+
+#: src/components/modal/index.tsx:255
+#, c-format
+msgid "Import"
+msgstr ""
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr ""
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr ""
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
#, c-format
-msgid "is not the same as the current access token"
+msgid "Field"
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:356
#, c-format
-msgid "cannot be empty"
+msgid "In the form"
msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:359
#, c-format
-msgid "cannot be the same as the old token"
+msgid "Reported"
msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:366
#, c-format
-msgid "is not the same"
+msgid "Type"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, fuzzy, c-format
+msgid "Account id"
+msgstr "Importo"
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, c-format
+msgid "Is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:542
+#, c-format
+msgid "Can't be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:546
+#, c-format
+msgid "Is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:554
#, c-format
msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "access token currently in use"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, c-format
msgid "New access token"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:578
#, c-format
-msgid "next access token to be used"
+msgid "Next access token to be used"
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, c-format
msgid "Repeat access token"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:584
#, c-format
-msgid "confirm the same access token"
+msgid "Confirm the same access token"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, c-format
msgid "Clearing the access token will mean public access to the instance"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:616
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Can't be the same as the old access token"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
"backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, c-format
+msgid "Your instance not found"
+msgstr ""
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/paths/login/index.tsx:95
+#, c-format
+msgid "Please enter your access token for %1$s."
+msgstr ""
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/admin/list/TableActive.tsx:100
#, c-format
-msgid "add new instance"
+msgid "Add new instance"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, c-format
+msgid "This is not a valid host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, c-format
+msgid "Invalid url"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, fuzzy, c-format
+msgid "Account:"
+msgstr "Importo"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, c-format
+msgid "Do not change"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -267,57 +859,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -360,66 +1002,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
-#, c-format
-msgid "Product"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "search products by it's description or id"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:151
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "no products found with that description"
+msgid "no match found with that description or id"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, c-format
+msgid "Search product"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:113
#, c-format
-msgid "how many products will be added"
+msgid "How many products will be added"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -432,54 +1069,74 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, c-format
+msgid "Invalid"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr "Importo"
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -487,12 +1144,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -502,7 +1159,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -512,2213 +1169,2399 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "amount in the current currency"
+msgid "Amount in the current currency."
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:211
-#, c-format
-msgid "Taxes"
-msgstr ""
-
-#: src/components/product/ProductList.tsx:38
+#: src/components/product/NonInventoryProductForm.tsx:208
#, c-format
-msgid "image"
+msgid "How many products will be added."
msgstr ""
-#: src/components/product/ProductList.tsx:41
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "description"
+msgid "Taxes"
msgstr ""
-#: src/components/product/ProductList.tsx:44
+#: src/components/product/ProductList.tsx:46
#, c-format
-msgid "quantity"
+msgid "Unit price"
msgstr ""
-#: src/components/product/ProductList.tsx:47
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "unit price"
+msgid "Total price"
msgstr ""
-#: src/components/product/ProductList.tsx:50
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "total price"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:153
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "required"
+msgid "Refund deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "not valid"
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:188
#, c-format
-msgid "must be greater than 0"
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
+#: src/paths/instance/orders/create/CreatePage.tsx:196
#, c-format
-msgid "not a valid json"
+msgid "Must have a refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:170
+#: src/paths/instance/orders/create/CreatePage.tsx:201
#, c-format
-msgid "should be in the future"
+msgid "Auto refund can't be after refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:173
+#: src/paths/instance/orders/create/CreatePage.tsx:208
#, c-format
-msgid "refund deadline cannot be before pay deadline"
+msgid "Must be in the future"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:179
+#: src/paths/instance/orders/create/CreatePage.tsx:376
#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+msgid "Simple"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:190
+#: src/paths/instance/orders/create/CreatePage.tsx:388
#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+msgid "Advanced"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:197
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
-msgid "should have a refund deadline"
+msgid "Manage products in order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:202
+#: src/paths/instance/orders/create/CreatePage.tsx:404
#, c-format
-msgid "auto refund cannot be after refund deadline"
+msgid "%1$s products with a total price of %2$s."
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
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:437
+#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
-msgid "final order price"
+msgid "Final order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "address where the products will be delivered"
+msgid "Address where the products will be delivered"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, c-format
-msgid "Payment deadline"
+msgid "Payment time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, c-format
msgid ""
-"Deadline for the customer to pay for the offer before it expires. Inventory "
-"products will be reserved until this deadline."
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:486
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, c-format
-msgid "Refund deadline"
+msgid "Default"
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:561
+#, fuzzy, c-format
+msgid "Refund time"
+msgstr "Rimborsato"
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
msgid ""
-"Time until which the wallet will automatically check for refunds without "
-"user interaction."
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:502
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Maximum deposit fee"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, 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."
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:507
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Maximum wire fee"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Wire fee amortization"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:55
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, c-format
-msgid "days"
+msgid "Additional information"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:65
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
-msgid "hours"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "minutes"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "seconds"
+msgid "Custom field name"
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "forever"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:796
#, c-format
-msgid "%1$sM"
+msgid "No deadline"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sY"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "%1$sd"
+msgid "Could not create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$sh"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:70
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$smin"
+msgid "No more stock for product with id \"%1$s\"."
msgstr ""
-#: src/components/form/InputDuration.tsx:72
-#, c-format
-msgid "%1$ssec"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:75
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "create order"
+msgid "Create order"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
+msgid "Load first page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr "Data"
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, 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
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "invalid format"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:301
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "this value exceed the refundable amount"
+msgid "Load next page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:346
+#: src/paths/instance/orders/list/Table.tsx:233
#, c-format
-msgid "date"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "amount"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "reason"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "amount to be refunded"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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
+#: src/paths/instance/orders/list/Table.tsx:391
#, c-format
-msgid "requested by the customer"
+msgid "Requested by the customer"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "other"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "why this order is being refunded"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "more information to give context"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "total price for the transaction"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "Max wire fee"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize "
-"wire fees on average"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Created at"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "time when this contract was generated"
+msgid "Payment deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:116
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:122
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, c-format
-msgid "where the order will be delivered"
+msgid "Where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "claimed"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "claimed at"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/details/DetailPage.tsx:465
#, c-format
-msgid "wired"
+msgid "Wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
-msgstr ""
+msgid "Refunded"
+msgstr "Rimborsato"
-#: src/paths/instance/orders/details/DetailPage.tsx:480
-#, c-format
-msgid "refund order"
-msgstr ""
+#: src/paths/instance/orders/details/DetailPage.tsx:490
+#, fuzzy, c-format
+msgid "Refund order"
+msgstr "Rimborsato"
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "not refundable"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, c-format
msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:636
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "unpaid"
+msgid "Unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:654
+#: src/paths/instance/orders/details/DetailPage.tsx:659
#, c-format
-msgid "pay at"
+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
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/orders/details/DetailPage.tsx:716
#, c-format
msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
msgid "Back"
msgstr "Indietro"
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/paths/instance/orders/details/index.tsx:88
#, c-format
-msgid "refund created successfully"
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "could not create the refund"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "select date to show nearby orders"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "order id"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "remove all filters"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "only show paid orders"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "Paid"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:142
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "only show orders with refunds"
+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
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
msgid ""
-"only show orders where customers paid, but wire payments from payment "
+"Only show orders where customers paid, but wire payments from payment "
"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "clear date filter"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "Enter an order id"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "order not found"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "could not get the order to refund"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/orders/list/index.tsx:114
#, c-format
-msgid "Loading..."
+msgid "Order id"
msgstr ""
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid ""
-"click here to configure the stock of the product, leave it as is and the "
-"backend will not control stock"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/components/form/InputStock.tsx:109
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Manage stock"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/components/form/InputStock.tsx:115
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "this product has been configured without stock control"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/components/form/InputStock.tsx:119
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "Infinite"
+msgid "Internal id on the system"
msgstr ""
-#: src/components/form/InputStock.tsx:136
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Incoming"
+msgid "Verification algorithm"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Lost"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Current"
+msgid "Device key"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid "remove stock control for this product"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "without stock"
+msgid "Your device need to have exactly the same value"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "Next restock"
+msgid "Generate random secret key"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "Delivery address"
+msgid "Random"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "illustration of the product for customers"
+msgid "Device added successfully"
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "product description for customers"
+msgid "Could not add device"
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "Age restricted"
+msgid "OTP Devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/paths/instance/otp_devices/list/Table.tsx:62
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Add new devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid ""
-"sale price for customers, including taxes, for above units of the product"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "Stock"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid ""
-"product inventory for products with finite supply (for internal use only)"
+msgid "There is no devices yet, add more pressing the + sign"
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Need to complete marked fields"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "could not create product"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "Products"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:73
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "add product to inventory"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "Sell"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "Profit"
+msgid "Template id is unknown"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Sold"
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "free"
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "go to product update page"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "Update"
+msgid "This product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "remove this product from the database"
+msgid "Infinite"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "update the product with new price"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/components/form/InputStock.tsx:169
#, c-format
-msgid "update product with new price"
+msgid "Incoming"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/components/form/InputStock.tsx:170
#, c-format
-msgid "add more elements to the inventory"
+msgid "Lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/components/form/InputStock.tsx:185
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Current"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "new price for the product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/components/form/InputStock.tsx:195
#, c-format
-msgid "the are value with errors"
+msgid "without stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/components/form/InputStock.tsx:204
#, c-format
-msgid "update product with new stock and price"
+msgid "Next restock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "There is no products yet, add more pressing the + sign"
+msgid "Warehouse address"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:86
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product updated successfully"
+msgid "Add element to the list"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/components/product/ProductForm.tsx:120
#, c-format
-msgid "could not update the product"
+msgid "Invalid amount"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "product delete successfully"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/paths/instance/products/list/index.tsx:109
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "could not delete the product"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Product id:"
+msgid "Product description for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/components/product/ProductForm.tsx:197
#, 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."
+msgid "Age restriction"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/components/product/ProductForm.tsx:198
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Is this product restricted for customer below certain age?"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/components/product/ProductForm.tsx:199
#, c-format
-msgid "it should be greater than 0"
+msgid "Minimum age of the customer"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/components/product/ProductForm.tsx:203
#, c-format
-msgid "must be a valid URL"
+msgid "Unit name"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/components/product/ProductForm.tsx:204
#, c-format
-msgid "Initial balance"
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "balance prior to deposit"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid "Exchange URL"
+msgid "Price per unit"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/components/product/ProductForm.tsx:210
#, c-format
-msgid "URL of exchange"
+msgid ""
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/components/product/ProductForm.tsx:214
#, c-format
-msgid "Next"
+msgid "Stock"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid "Wire method"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "method to use for wire transfer"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Select one wire method"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/components/product/ProductForm.tsx:231
#, c-format
-msgid "could not create reserve"
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/components/product/ProductForm.tsx:232
#, c-format
-msgid "Valid until"
+msgid "Categories where this product will be listed on."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "Created balance"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Exchange balance"
+msgid "Could not create product"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "Picked up"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Committed"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Account address"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
-msgid "Subject"
-msgstr "Soggetto"
+msgid "Sold"
+msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "Tips"
+msgid "Free"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
-msgid "Authorized"
+msgid "Update"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/products/list/Table.tsx:283
#, c-format
-msgid "Expiration"
+msgid "Remove this product from the database"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "amount of tip"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "Justification"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/products/list/Table.tsx:373
#, c-format
-msgid "reason for the tip"
+msgid "Update product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/products/list/Table.tsx:384
#, c-format
-msgid "URL after tip"
+msgid "Confirm update"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "Reserves not yet funded"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "Reserves ready"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "add new reserve"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "Expires at"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "Initial"
+msgid "There is no products yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid ""
-"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Product updated successfully"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid "Expected Balance"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "could not create the tip"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/products/list/index.tsx:149
#, c-format
-msgid "should not be empty"
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "should be greater that 0"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "can't be empty"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:85
+#, c-format
+msgid "Product (ID: %1$s) has been updated"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "to short"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "Invalid. only characters and numbers"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:112
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Must be greater that 0"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/create/CreatePage.tsx:119
#, c-format
-msgid "size of the key should be 32"
+msgid "To short"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
msgid "Identifier"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Fixed summary"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Fixed price"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, c-format
+msgid "Amount is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
msgid "Minimum age"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, 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
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Verification algorithm"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "Point-of-sale key"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Useful to validate the purchase"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "generate random secret key"
+msgid "No device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "random"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "show secret key"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "hide secret key"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "hide"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/paths/instance/templates/list/Table.tsx:66
#, c-format
-msgid "show"
+msgid "Add new templates"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "could not inform template"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "Amount is required"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid "Order summary is required"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "New order for template"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "Amount of the order"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "Order summary"
+msgid "Load more templates after the last one"
+msgstr ""
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/templates/list/index.tsx:91
#, c-format
-msgid "could not create order from template"
+msgid "Jump to template with the given template ID"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/instance/templates/list/index.tsx:92
#, 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."
+msgid "Template identification"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:132
+#, c-format
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/paths/instance/templates/list/index.tsx:137
#, c-format
-msgid "Fixed amount"
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Default amount"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/templates/list/index.tsx:160
#, c-format
-msgid "Default summary"
+msgid "Deleting an template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/templates/list/index.tsx:162
+#, c-format
+msgid "can't be undone"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
msgid "Print"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Setup TOTP"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "Templates"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "add new templates"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "load more templates before the first one"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "load newer templates"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "delete selected templates from the database"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "use template to create new order"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid "create qr code for the template"
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "load more templates after the last one"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "load older templates"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/token/DetailPage.tsx:121
#, c-format
-msgid "could not delete the template"
+msgid "Current access token"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "could not update template"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/token/DetailPage.tsx:142
#, c-format
-msgid "should be one of '%1$s'"
+msgid "Clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/token/DetailPage.tsx:177
#, c-format
-msgid "Webhook ID to use"
+msgid "Confirm change"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "Event"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "Method"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "Method used by the webhook"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "URL"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "Header"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Header template of the webhook"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "Body"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Body template by the webhook"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Webhooks"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "add new webhooks"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Duration"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "load newer webhooks"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "Event type"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "delete selected webhook from the database"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "load older webhooks"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "Go to token family update page"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
+#, c-format
+msgid "Remove this token family from the database"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
+#, c-format
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
+#, c-format
+msgid "Token family updated successfully"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
+#, c-format
+msgid "Could not update the token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, c-format
+msgid "Failed to delete token family"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
#, c-format
-msgid "webhook delete successfully"
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, c-format
+msgid "Token familty updated successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
#, c-format
-msgid "could not delete the webhook"
+msgid "Could not update token family"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
#, c-format
-msgid "check the id, does not look valid"
+msgid "Check the id, does not look valid"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Must have 52 characters, current %1$s"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
#, 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
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
msgid "Amount credited"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "could not inform transfer"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Transfers"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "add new transfer"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/transfers/list/Table.tsx:64
#, c-format
-msgid "load more transfers before the first one"
+msgid "Add new transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "load newer transfers"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "unknown"
+msgid "never"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "delete selected transfer from the database"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "load more transfer after the last one"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "load older transfers"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/paths/instance/transfers/list/ListPage.tsx:76
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, c-format
-msgid "filter by account address"
+msgid "All accounts"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/paths/instance/transfers/list/ListPage.tsx:105
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Only show wire transfers confirmed by the merchant"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/paths/instance/transfers/list/ListPage.tsx:115
+#, c-format
+msgid "Only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
msgid "Unverified"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid "is not valid"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid "is not a number"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "must be 1 or greater"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "max 7 lines"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "change authorization configuration"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+msgid "Doesn't match"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:82
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Enable access control"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "IBAN country code not found"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Target type"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Business name"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "Routing"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "Routing number."
+msgid "Email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "Account"
+msgid "Contact email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "Account number."
+msgid "Website URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "Business Identifier Code."
+msgid "URL."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "Bank Account Number."
+msgid "Logo"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "Unified Payment Interface."
+msgid "Logo image."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "Bitcoin protocol."
+msgid "Physical location of the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "Ethereum protocol."
+msgid "Jurisdiction"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Interledger protocol."
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "Host"
+msgid "Pay transaction fee"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "Bank host."
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Bank account."
+msgid "Default payment delay"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "Bank account owner's name."
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "No accounts yet."
+msgid "Default wire transfer delay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it "
-"is used to administer other instances."
+"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/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "Business name"
+msgid "Instance id"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/update/index.tsx:108
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Failed to update instance"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "Email"
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "Contact email"
+msgid "Must be one of '%1$s'"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "Website URL"
+msgid "Webhook ID to use"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "URL."
+msgid "Event"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "Logo"
+msgid "Pay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Logo image."
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Bank account"
+msgid "Method"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "GET"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Default max deposit fee"
+msgid "POST"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "PUT"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Default max wire fee"
+msgid "PATCH"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
+msgid "HEAD"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Default wire fee amortization"
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Physical location of the merchant."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Jurisdiction"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "order's description"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
#, c-format
-msgid "Default payment delay"
+msgid "order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
+msgid "order's unique identification"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Default wire transfer delay"
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, 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."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "Instance id"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Http body"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Manage access token"
+msgid "Body template by the webhook"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Failed to create instance"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Login required"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Please enter your access token."
+msgid "Could not create webhook"
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "Access Token"
+msgid "Webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "Add new webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Event type"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Access denied"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid "The access token provided is invalid."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Webhook delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "The access token provided is invalid"
+msgid "Header"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid "Hide for today"
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Instance"
+msgid "Body"
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Settings"
-msgstr "Impostazioni"
+msgid "Webhook updated"
+msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Connection"
+msgid "Could not update webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "New"
+msgid "Language"
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "List"
+msgid "Set default"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Log out"
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, c-format
+msgid "Date format"
+msgstr ""
+
+#: src/paths/settings/index.tsx:131
+#, c-format
+msgid "How the date is going to be displayed"
+msgstr ""
+
+#: src/paths/settings/index.tsx:134
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/paths/settings/index.tsx:135
+#, c-format
+msgid ""
+"Shows more options and tools which are not intended for general audience."
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:133
+#, c-format
+msgid "Total products"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, c-format
+msgid "Delete selected category from the database"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, c-format
+msgid "Category delete successfully"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:95
+#, c-format
+msgid "Could not delete the category"
+msgstr ""
+
+#: src/paths/instance/categories/create/CreatePage.tsx:77
+#, c-format
+msgid "Category name"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:53
+#, c-format
+msgid "Category added successfully"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:59
+#, c-format
+msgid "Could not add category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
+#, c-format
+msgid "Id:"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, c-format
+msgid "Name of the category"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
#, c-format
-msgid "Check your token is valid"
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, c-format
+msgid "Search by product description or id"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
#, c-format
-msgid "Couldn't access the server."
+msgid "Products that this category will list."
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/paths/instance/categories/update/index.tsx:93
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Could not update category"
msgstr ""
-#: src/Application.tsx:104
+#: src/paths/instance/categories/update/index.tsx:95
#, c-format
-msgid "Server not found"
+msgid "Category id is unknown"
msgstr ""
-#: src/Application.tsx:118
+#: src/Routing.tsx:659
#, c-format
-msgid "Server response with an error code"
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
msgstr ""
-#: src/Application.tsx:120
+#: src/Routing.tsx:703
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "KYC verification needed"
msgstr ""
-#: src/Application.tsx:131
+#: src/Routing.tsx:707
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
msgstr ""
-#: src/Application.tsx:144
+#: src/components/menu/SideBar.tsx:157
#, c-format
-msgid "Unexpected Error"
+msgid "Configuration"
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/components/menu/SideBar.tsx:196
+#, c-format
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: src/components/menu/SideBar.tsx:206
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Access token"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/components/menu/SideBar.tsx:214
#, c-format
-msgid "add element to the list"
+msgid "Connection"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/components/menu/SideBar.tsx:223
#, c-format
-msgid "add"
+msgid "Interface"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:264
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:283
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/paths/admin/create/index.tsx:54
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/Application.tsx:208
+#, c-format
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, c-format
+msgid "Contacting the server failed"
+msgstr ""
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2731,12 +3574,26 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
+
+#, c-format
+#~ msgid "Subject"
+#~ msgstr "Soggetto"
diff --git a/packages/merchant-backoffice-ui/src/i18n/strings.ts b/packages/merchant-backoffice-ui/src/i18n/strings.ts
index 65dc41358..b036d57af 100644
--- a/packages/merchant-backoffice-ui/src/i18n/strings.ts
+++ b/packages/merchant-backoffice-ui/src/i18n/strings.ts
@@ -17,1614 +17,1616 @@
/*eslint quote-props: ["error", "consistent"]*/
export const strings: {[s: string]: any} = {};
-strings['de'] = {
- "domain": "messages",
+strings['uk'] = {
"locale_data": {
"messages": {
"": {
"domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ "plural_forms": "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;",
+ "lang": "uk"
},
"Cancel": [
- ""
+ "Скасувати"
],
"%1$s": [
- ""
+ "%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": [
- ""
+ "Ви оновлюєте токен доступу з інстанції з ідентифікатором %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": [
- ""
+ "Операція буде автоматично скасована через %1$s секунд"
],
"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": [
- ""
+ "Інстанція \"%1$s\" (ID: %2$s) була видалена"
],
"Failed to delete instance": [
- ""
+ "Не вдалося видалити інстанцію"
],
"Instance '%1$s' (ID: %2$s) has been disabled": [
- ""
+ "Інстанція '%1$s' (ID: %2$s) була деактивована"
],
"Failed to purge instance": [
- ""
+ "Не вдалося очистити інстанцію"
],
"Pending KYC verification": [
- ""
+ "Очікування перевірки KYC"
],
"Timed out": [
- ""
+ "Час очікування вичерпано"
],
"Exchange": [
- ""
+ "Exchange"
],
"Target account": [
- ""
+ "Цільовий рахунок"
],
"KYC URL": [
- ""
+ "KYC URL"
],
"Code": [
- ""
+ "Код"
],
"Http Status": [
- ""
+ "HTTP статус"
],
"No pending kyc verification!": [
- ""
+ "Немає очікуваних перевірок KYC!"
],
"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!": [
- ""
+ "Кількість має бути більше 0!"
],
"This quantity exceeds remaining stock. Currently, only %1$s units remain unreserved in stock.": [
- ""
+ "Ця кількість перевищує залишок на складі. Наразі на складі залишилося лише %1$s одиниць, які не зарезервовані."
],
"Quantity": [
- ""
+ "Кількість"
],
"how many products will be added": [
- ""
+ "скільки продуктів буде додано"
],
"Add from inventory": [
- ""
+ "Додати зі складу"
],
"Image should be smaller than 1 MB": [
- ""
+ "Зображення повинно бути меншим за 1 МБ"
],
"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;.": [
- ""
+ "Введіть валюту та значення, розділені двокрапкою, наприклад, &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": [
- ""
+ "має бути більше 0"
],
"not a valid json": [
- ""
+ "недійсний 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 виконання"
],
"URL to which the user will be redirected after successful payment.": [
- ""
+ "URL, на який користувача буде перенаправлено після успішної оплати."
],
"Taler payment options": [
- ""
+ "Опції оплати Taler"
],
"Override default Taler payment settings for this order": [
- ""
+ "Перевизначити стандартні налаштування оплати Taler для цього замовлення"
],
"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": [
- ""
+ "Будь-яке значення більше 0 обмежуватиме монети, які можуть бути використані для оплати цього контракту. Якщо порожнє, вікове обмеження визначатиметься продуктами"
],
"Min age defined by the producs is %1$s": [
- ""
+ "Мінімальний вік, визначений продуктами, становить %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).": [
- ""
+ "Ви повинні ввести значення у форматі JavaScript Object Notation (JSON)."
],
"days": [
- ""
+ "дні"
],
"hours": [
- ""
+ "години"
],
"minutes": [
- ""
+ "хвилини"
],
"seconds": [
- ""
+ "секунди"
],
"forever": [
- ""
+ "назавжди"
],
"%1$sM": [
- ""
+ "%1$sМ"
],
"%1$sY": [
- ""
+ "%1$sР"
],
"%1$sd": [
- ""
+ "%1$sдн."
],
"%1$sh": [
- ""
+ "%1$sг"
],
"%1$smin": [
- ""
+ "%1$sхв"
],
"%1$ssec": [
- ""
+ "%1$sсек"
],
"Orders": [
- ""
+ "Замовлення"
],
"create order": [
- ""
+ "створити замовлення"
],
"load newer orders": [
- ""
+ "завантажити нові замовлення"
],
"Date": [
- ""
+ "Дата"
],
"Refund": [
- ""
+ "Повернення"
],
"copy url": [
- ""
+ "скопіювати 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": [
- ""
+ "URL для цієї покупки"
],
"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": [
- ""
+ "URL статусу"
],
"Refund URI": [
- ""
+ "URI повернення"
],
"unpaid": [
- ""
+ "неоплачено"
],
"pay at": [
- ""
+ "оплачено о"
],
"created at": [
- ""
+ "створено о"
],
"Order status URL": [
- ""
+ "URL статусу замовлення"
],
"Payment URI": [
- ""
+ "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)": [
- ""
+ "втрати не можуть бути більшими за поточні та прибуваючі (макс %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)": [
- ""
+ "ідентифікація продукту для використання в URL (тільки для внутрішнього використання)"
],
"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": [
- ""
+ "одиниця, що описує кількість проданого продукту (наприклад, 2 кілограми, 5 літрів, 3 предмети, 5 метрів) для клієнтів"
],
"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:": [
- ""
+ "Якщо ваша система підтримує RFC 8905, ви можете зробити це, відкривши цей URI:"
],
"it should be greater than 0": [
- ""
+ "це повинно бути більше 0"
],
"must be a valid URL": [
- ""
+ "повинен бути дійсний URL"
],
"Initial balance": [
- ""
+ "Початковий баланс"
],
"balance prior to deposit": [
- ""
+ "баланс до внесення депозиту"
],
"Exchange URL": [
- ""
+ "URL обмінника"
],
"URL of exchange": [
- ""
+ "URL обмінника"
],
"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 після чайових"
],
"URL to visit after tip payment": [
- ""
+ "URL для відвідування після оплати чайових"
],
"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": [
- ""
+ "повинно бути більше 0"
],
"can't be empty": [
- ""
+ "не може бути порожнім"
],
"to short": [
- ""
+ "занадто короткий"
],
"just letters and numbers from 2 to 7": [
- ""
+ "лише літери та цифри від 2 до 7"
],
"size of the key should be 32": [
- ""
+ "розмір ключа повинен бути 32"
],
"Identifier": [
- ""
+ "Ідентифікатор"
],
"Name of the template in URLs.": [
- ""
+ "Назва шаблону в URL."
],
"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": [
- ""
+ "Налаштування 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": [
- ""
+ "створити QR-код для шаблону"
],
"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'": [
- ""
+ "повинно бути одним із '%1$s'"
],
"Webhook ID to use": [
- ""
+ "ID вебхука для використання"
],
"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": [
- ""
+ "URL вебхука, куди буде перенаправлений клієнт"
],
"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": [
- ""
+ "повинно бути 52 символи, поточний %1$s"
],
"URL doesn't have the right format": [
- ""
+ "URL має неправильний формат"
],
"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": [
- ""
+ "унікальний ідентифікатор банківського переказу, що використовується обмінником, має бути довжиною 52 символи"
],
"Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
- ""
+ "Основний URL обмінника, який здійснив переказ, має бути в призначенні банківського переказу"
],
"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": [
- ""
+ "має бути 1 або більше"
],
"max 7 lines": [
- ""
+ "максимум 7 рядків"
],
"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.": [
- ""
+ "Це недійсна адреса Ethereum."
],
"IBAN numbers usually have more that 4 digits": [
- ""
+ "Номера IBAN зазвичай мають більше 4-ьох цифр"
],
"IBAN numbers usually have less that 34 digits": [
- ""
+ "Номера IBAN зазвичай мають менше 34-ьох цифр"
],
"IBAN country code not found": [
- ""
+ "Код країни IBAN не знайдено"
],
"IBAN number is not valid, checksum is wrong": [
- ""
+ "Номер IBAN не коректний, контрольна сума не сходиться"
],
"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.": [
- ""
+ "Протокол Ethereum."
],
"Interledger protocol.": [
- ""
+ "Протокол Interledger."
],
"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.": [
- ""
+ "Назва інстанції в URL. Інстанція 'default' є особливою, оскільки використовується для адміністрування інших інстанцій."
],
"Business name": [
- ""
+ "Назва бізнесу"
],
"Legal name of the business represented by this instance.": [
- ""
+ "Юридична назва бізнесу, який представляє ця інстанція."
],
"Email": [
- ""
+ "Email"
],
"Contact email": [
- ""
+ "Контактний email"
],
"Website URL": [
- ""
+ "URL вебсайту"
],
"URL.": [
- ""
+ "URL."
],
"Logo": [
- ""
+ "Логотип"
],
"Logo image.": [
- ""
+ "Зображення логотипу."
],
"Bank account": [
- ""
+ "Банківський рахунок"
],
"URI specifying bank account for crediting revenue.": [
- ""
+ "URI, що вказує на банківський рахунок для зарахування доходу."
],
"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\"": [
- ""
+ "Діагностика від %1$s: \"%2$s\""
],
"The backend reported a problem: HTTP status #%1$s": [
- ""
+ "Бекенд повідомив про проблему: HTTP статус #%1$s"
],
"Diagnostic from %1$s is '%2$s'": [
- ""
+ "Діагностика від %1$s: '%2$s'"
],
"Access denied": [
- ""
+ "Доступ заборонено"
],
"The access token provided is invalid.": [
- ""
+ "Наданий токен доступу є недійсним."
],
"No 'default' instance configured yet.": [
- ""
+ "Інстанція 'default' ще не налаштована."
],
"Create a 'default' instance to begin using the merchant backoffice.": [
- ""
+ "Створіть інстанцію 'default', щоб почати використовувати бекофіс продавця."
],
"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": [
- ""
+ "Не вдалося визначити ідентифікатор інстанції з URL %1$s"
],
"Server not found": [
- ""
+ "Сервер не знайдено"
],
"Server response with an error code": [
- ""
+ "Відповідь сервера з кодом помилки"
],
"Got message %1$s from %2$s": [
- ""
+ "Отримано повідомлення %1$s від %2$s"
],
"Response from server is unreadable, http status: %1$s": [
- ""
+ "Відповідь від сервера не читається, HTTP статус: %1$s"
],
"Unexpected Error": [
- ""
+ "Несподівана помилка"
],
"The value %1$s is invalid for a payment url": [
- ""
+ "Значення %1$s є недійсним для URL оплати"
],
"add element to the list": [
- ""
+ "додати елемент до списку"
],
"add": [
- ""
+ "додати"
],
"Deleting": [
- ""
+ "Видалення"
],
"Changing": [
- ""
+ "Зміна"
],
"Order ID": [
- ""
+ "Ідентифікатор замовлення"
],
"Payment URL": [
- ""
+ "URL оплати"
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;",
+ "lang": "uk",
+ "completeness": 100
};
-strings['en'] = {
- "domain": "messages",
+strings['sv'] = {
"locale_data": {
"messages": {
"": {
@@ -3226,461 +3228,464 @@ strings['en'] = {
""
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": "",
+ "completeness": 0
};
-strings['es'] = {
- "domain": "messages",
+strings['it'] = {
"locale_data": {
"messages": {
"": {
"domain": "messages",
"plural_forms": "nplurals=2; plural=n != 1;",
- "lang": "es"
+ "lang": "it"
},
"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"
+ "Importo"
],
"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": [
""
@@ -3689,7 +3694,7 @@ strings['es'] = {
""
],
"Additional information": [
- "Información extra"
+ ""
],
"Custom information to be included in the contract for this order.": [
""
@@ -3698,19 +3703,19 @@ strings['es'] = {
""
],
"days": [
- "días"
+ ""
],
"hours": [
- "horas"
+ ""
],
"minutes": [
- "minutos"
+ ""
],
"seconds": [
- "segundos"
+ ""
],
"forever": [
- "nunca"
+ ""
],
"%1$sM": [
""
@@ -3731,91 +3736,91 @@ strings['es'] = {
""
],
"Orders": [
- "Órdenes"
+ ""
],
"create order": [
- "creado"
+ ""
],
"load newer orders": [
- "cargar nuevas ordenes"
+ ""
],
"Date": [
- "Fecha"
+ "Data"
],
"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": [
""
@@ -3824,7 +3829,7 @@ strings['es'] = {
""
],
"Created at": [
- "Creado en"
+ ""
],
"time when this contract was generated": [
""
@@ -3845,100 +3850,100 @@ strings['es'] = {
""
],
"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": [
- ""
+ "Indietro"
],
"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": [
""
@@ -3950,19 +3955,19 @@ strings['es'] = {
""
],
"Paid": [
- "Pagado"
+ ""
],
"only show orders with refunds": [
- "No se pudo create el reembolso"
+ ""
],
"Refunded": [
- "Reembolzado"
+ "Rimborsato"
],
"only show orders where customers paid, but wire payments from payment provider are still pending": [
""
],
"Not wired": [
- "No transferido"
+ ""
],
"clear date filter": [
""
@@ -3971,52 +3976,52 @@ strings['es'] = {
""
],
"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)": [
""
@@ -4040,7 +4045,7 @@ strings['es'] = {
""
],
"Stock": [
- "Existencias"
+ ""
],
"product inventory for products with finite supply (for internal use only)": [
""
@@ -4052,31 +4057,31 @@ strings['es'] = {
""
],
"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": [
""
@@ -4094,7 +4099,7 @@ strings['es'] = {
""
],
"new price for the product": [
- "no se pudo actualizar el producto"
+ ""
],
"the are value with errors": [
""
@@ -4103,22 +4108,22 @@ strings['es'] = {
""
],
"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.": [
""
@@ -4127,76 +4132,76 @@ strings['es'] = {
""
],
"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"
+ "Soggetto"
],
"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": [
""
@@ -4208,13 +4213,13 @@ strings['es'] = {
""
],
"Reserves not yet funded": [
- "Servidor no encontrado"
+ ""
],
"Reserves ready": [
""
],
"add new reserve": [
- "cargar nuevas transferencias"
+ ""
],
"Expires at": [
""
@@ -4229,22 +4234,22 @@ strings['es'] = {
""
],
"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": [
""
@@ -4265,25 +4270,25 @@ strings['es'] = {
""
],
"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.": [
""
@@ -4319,37 +4324,37 @@ strings['es'] = {
""
],
"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": [
""
@@ -4367,7 +4372,7 @@ strings['es'] = {
""
],
"load newer templates": [
- "cargar nuevas transferencias"
+ ""
],
"delete selected templates from the database": [
""
@@ -4376,28 +4381,28 @@ strings['es'] = {
""
],
"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": [
""
@@ -4415,7 +4420,7 @@ strings['es'] = {
""
],
"URL": [
- "URL"
+ ""
],
"URL of the webhook where the customer will be redirected": [
""
@@ -4442,7 +4447,7 @@ strings['es'] = {
""
],
"load newer webhooks": [
- "cargar nuevas ordenes"
+ ""
],
"Event type": [
""
@@ -4454,25 +4459,25 @@ strings['es'] = {
""
],
"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": [
""
@@ -4484,7 +4489,7 @@ strings['es'] = {
""
],
"Wire transfer ID": [
- "Id de transferencia"
+ ""
],
"unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
""
@@ -4499,55 +4504,55 @@ strings['es'] = {
""
],
"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": [
""
@@ -4556,61 +4561,61 @@ strings['es'] = {
""
],
"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.": [
""
@@ -4619,7 +4624,7 @@ strings['es'] = {
""
],
"Unified Payment Interface.": [
- "Interfaz de pago unificado."
+ ""
],
"Bitcoin protocol.": [
""
@@ -4649,7 +4654,7 @@ strings['es'] = {
""
],
"Business name": [
- "Nombre de edificio"
+ ""
],
"Legal name of the business represented by this instance.": [
""
@@ -4661,7 +4666,7 @@ strings['es'] = {
""
],
"Website URL": [
- "URL de sitio web"
+ ""
],
"URL.": [
""
@@ -4673,25 +4678,25 @@ strings['es'] = {
""
],
"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.": [
""
@@ -4700,43 +4705,43 @@ strings['es'] = {
""
],
"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": [
""
@@ -4745,19 +4750,19 @@ strings['es'] = {
""
],
"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.": [
""
@@ -4769,104 +4774,107 @@ strings['es'] = {
""
],
"Instance": [
- "Instancia"
+ ""
],
"Settings": [
- "Configuración"
+ "Impostazioni"
],
"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"
+ ""
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "it",
+ "completeness": 1
};
strings['fr'] = {
- "domain": "messages",
"locale_data": {
"messages": {
"": {
"domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ "plural_forms": "nplurals=2; plural=(n!=1);",
+ "lang": "fr"
},
"Cancel": [
- ""
+ "Annuler"
],
"%1$s": [
""
],
"Close": [
- ""
+ "Fermer"
],
"Continue": [
- ""
+ "Continuer"
],
"Clear": [
""
],
"Confirm": [
- ""
+ "Confirmer"
],
"is not the same as the current access token": [
""
],
"cannot be empty": [
- ""
+ "ne peux pas être vide"
],
"cannot be the same as the old token": [
""
@@ -6438,11 +6446,1623 @@ strings['fr'] = {
""
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n!=1);",
+ "lang": "fr",
+ "completeness": 0
};
-strings['it'] = {
+strings['es'] = {
+ "locale_data": {
+ "messages": {
+ "": {
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "es"
+ },
+ "Cancel": [
+ "Cancelar"
+ ],
+ "%1$s": [
+ "%1$s"
+ ],
+ "Close": [
+ "Cerrar"
+ ],
+ "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": [
+ "Borrar"
+ ],
+ "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": [
+ "Producto"
+ ],
+ "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 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 bancaria"
+ ],
+ "Deadline for the exchange to make the wire transfer.": [
+ "Plazo para que el proveedor haga la transferencia bancaria."
+ ],
+ "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 cartera 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.": [
+ "Máximo total de comisiones por transferencia que el vendedor está dispuesto a cubrir para este pedido. Los gastos de transferencia que superen este importe correrán a cargo del cliente."
+ ],
+ "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.": [
+ "Factor por el que se dividen los comisiones por transferencia que superan el umbral anterior para determinar la parte del exceso de comisiones por transferencia que debe pagar explícitamente el consumidor."
+ ],
+ "Create token": [
+ "Crear token"
+ ],
+ "Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.": [
+ "Desmarque esta opción si el backend del comerciante ha generado un ID de pedido con suficiente entropía para evitar reclamaciones de adversarios."
+ ],
+ "Minimum age required": [
+ "Edad mínima requerida"
+ ],
+ "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": [
+ "Cualquier valor superior a 0 limitará las monedas que se pueden utilizar para pagar este contrato. Si está vacío, la restricción de edad vendrá definida por los productos"
+ ],
+ "Min age defined by the producs is %1$s": [
+ "La edad mínima definida por el producto es%1$s"
+ ],
+ "Additional information": [
+ "Información adicional"
+ ],
+ "Custom information to be included in the contract for this order.": [
+ "Información personalizada que debe incluirse en el contrato para este pedido."
+ ],
+ "You must enter a value in JavaScript Object Notation (JSON).": [
+ "Debes introducir un valor en JavaScript Object Notation (JSON)."
+ ],
+ "days": [
+ "días"
+ ],
+ "hours": [
+ "horas"
+ ],
+ "minutes": [
+ "minutos"
+ ],
+ "seconds": [
+ "segundos"
+ ],
+ "forever": [
+ "por siempre"
+ ],
+ "%1$sM": [
+ "%1$sM"
+ ],
+ "%1$sY": [
+ "%1$sA"
+ ],
+ "%1$sd": [
+ "%1$sd"
+ ],
+ "%1$sh": [
+ "%1$sh"
+ ],
+ "%1$smin": [
+ "%1$smin"
+ ],
+ "%1$ssec": [
+ "%1$sseg"
+ ],
+ "Orders": [
+ "Órdenes"
+ ],
+ "create order": [
+ "crear orden"
+ ],
+ "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": [
+ "tasa máxima total de depósito aceptada por el comerciante para este contrato"
+ ],
+ "Max wire fee": [
+ "Impuesto de transferencia máximo"
+ ],
+ "maximum wire fee accepted by the merchant": [
+ "comisión máxima por transferencia aceptada por el comerciante"
+ ],
+ "over how many customer transactions does the merchant expect to amortize wire fees on average": [
+ "en cuántas transacciones de clientes espera el comerciante amortizar los gastos de transferencia por término medio"
+ ],
+ "Created at": [
+ "Creado en"
+ ],
+ "time when this contract was generated": [
+ "momento en que se generó este contrato"
+ ],
+ "after this deadline has passed no refunds will be accepted": [
+ "pasado este plazo no se aceptarán devoluciones"
+ ],
+ "after this deadline, the merchant won't accept payments for the contract": [
+ "pasado este plazo, el comerciante no aceptará pagos por el contrato"
+ ],
+ "transfer deadline for the exchange": [
+ "plazo de transferencia para el proveedor"
+ ],
+ "time indicating when the order should be delivered": [
+ "fecha en la que debe entregarse el pedido"
+ ],
+ "where the order will be delivered": [
+ "dónde se entregará el pedido"
+ ],
+ "Auto-refund delay": [
+ "Plazo de reembolso automático"
+ ],
+ "how long the wallet should try to get an automatic refund for the purchase": [
+ "cuánto tiempo debe intentar la cartera obtener el reembolso automático de la compra"
+ ],
+ "Extra info": [
+ "Información adicional"
+ ],
+ "extra data that is only interpreted by the merchant frontend": [
+ "datos adicionales que solo son interpretados por la interfaz del comerciante"
+ ],
+ "Order": [
+ "Orden"
+ ],
+ "claimed": [
+ "reclamado"
+ ],
+ "claimed at": [
+ "reclamado en"
+ ],
+ "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": [
+ "reembolsado"
+ ],
+ "not refundable": [
+ "No reembolsable"
+ ],
+ "refund": [
+ "reembolzar"
+ ],
+ "Refunded amount": [
+ "Monto reembolsado"
+ ],
+ "Refund taken": [
+ "Reembolsado"
+ ],
+ "Status URL": [
+ "Estado de la URL"
+ ],
+ "Refund URI": [
+ "URI de 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": [
+ "Regresar"
+ ],
+ "refund created successfully": [
+ "reembolzo creado satisfactoriamente"
+ ],
+ "could not create the refund": [
+ "No se pudo create el reembolso"
+ ],
+ "select date to show nearby orders": [
+ "seleccione la fecha para mostrar pedidos cercanos"
+ ],
+ "order id": [
+ "ID de la orden"
+ ],
+ "jump to order with the given order ID": [
+ "saltar al pedido con el ID de pedido proporcionado"
+ ],
+ "remove all filters": [
+ "eliminar todos los filtros"
+ ],
+ "only show paid orders": [
+ "mostrar sólo pedidos pagados"
+ ],
+ "Paid": [
+ "Pagado"
+ ],
+ "only show orders with refunds": [
+ "mostrar solo pedidos con reembolso"
+ ],
+ "Refunded": [
+ "Reembolsado"
+ ],
+ "only show orders where customers paid, but wire payments from payment provider are still pending": [
+ "mostrar sólo los pedidos en los que los clientes han pagado, pero los pagos por transferencia del proveedor de pago siguen pendientes"
+ ],
+ "Not wired": [
+ "No transferido"
+ ],
+ "clear date filter": [
+ "borrar filtro de fechas"
+ ],
+ "date (YYYY/MM/DD)": [
+ "Fecha(AAAA/MM/DD)"
+ ],
+ "Enter an order id": [
+ "Insertar un ID para el pedido"
+ ],
+ "order not found": [
+ "Orden no encontrada"
+ ],
+ "could not get the order to refund": [
+ "no se ha podido obtener el reembolso para el pedido"
+ ],
+ "Loading...": [
+ "Cargando..."
+ ],
+ "click here to configure the stock of the product, leave it as is and the backend will not control stock": [
+ "pulse aquí para configurar el stock del producto, déjelo como está y el backend no controlará el stock"
+ ],
+ "Manage stock": [
+ "Administrar stock"
+ ],
+ "this product has been configured without stock control": [
+ "este producto se ha configurado sin control de existencias"
+ ],
+ "Infinite": [
+ "Inifinito"
+ ],
+ "lost cannot be greater than current and incoming (max %1$s)": [
+ "la pérdida no puede ser mayor que la cantidad entrante actual (max %1$s )"
+ ],
+ "Incoming": [
+ "Ingresando"
+ ],
+ "Lost": [
+ "Perdido"
+ ],
+ "Current": [
+ "Actual"
+ ],
+ "remove stock control for this product": [
+ "eliminar el control de existencias de este producto"
+ ],
+ "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)": [
+ "Identificación del producto para usar en las URL (solo para uso interno)"
+ ],
+ "illustration of the product for customers": [
+ "ilustración del producto para los clientes"
+ ],
+ "product description for customers": [
+ "descripción del producto para los clientes"
+ ],
+ "Age restricted": [
+ "Restricción de edad"
+ ],
+ "is this product restricted for customer below certain age?": [
+ "¿este producto está restringido para clientes menores de cierta edad?"
+ ],
+ "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers": [
+ "unidad que describe la cantidad de producto vendido (por ejemplo, 2 kilogramos, 5 litros, 3 artículos, 5 metros) para los clientes"
+ ],
+ "sale price for customers, including taxes, for above units of the product": [
+ "precio de venta para los clientes, impuestos incluidos, por encima de las unidades del producto"
+ ],
+ "Stock": [
+ "Existencias"
+ ],
+ "product inventory for products with finite supply (for internal use only)": [
+ "inventario de productos para productos con suministro finito (sólo para uso interno)"
+ ],
+ "taxes included in the product price, exposed to customers": [
+ "impuestos incluidos en el precio del producto, expuestos a los clientes"
+ ],
+ "Need to complete marked fields": [
+ "Necesita completar los campos marcados"
+ ],
+ "could not create product": [
+ "no se pudo crear el producto"
+ ],
+ "Products": [
+ "Productos"
+ ],
+ "add product to inventory": [
+ "añadir producto al inventario"
+ ],
+ "Sell": [
+ "Venta"
+ ],
+ "Profit": [
+ "Ganancia"
+ ],
+ "Sold": [
+ "Vendido"
+ ],
+ "free": [
+ "Gratis"
+ ],
+ "go to product update page": [
+ "ir a la página de actualización del producto"
+ ],
+ "Update": [
+ "Actualizar"
+ ],
+ "remove this product from the database": [
+ "eliminar este producto de la base de datos"
+ ],
+ "update the product with new price": [
+ "actualizar el producto con el nuevo precio"
+ ],
+ "update product with new price": [
+ "actualizar producto con nuevo precio"
+ ],
+ "add more elements to the inventory": [
+ "añadir más elementos al inventario"
+ ],
+ "report elements lost in the inventory": [
+ "informar de elementos perdidos en el inventario"
+ ],
+ "new price for the product": [
+ "nuevo precio para el producto"
+ ],
+ "the are value with errors": [
+ "hay valores con errores"
+ ],
+ "update product with new stock and price": [
+ "actualizar el producto con nuevas existencias y precio"
+ ],
+ "There is no products yet, add more pressing the + sign": [
+ "No existen productos todavía, añadir más pulsando el símbolo +"
+ ],
+ "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.": [
+ "Para completar la configuración de la reserva, ahora debe iniciar una transferencia bancaria utilizando el asunto de transferencia bancaria indicado y abonando el importe especificado en la cuenta indicada del proveedor."
+ ],
+ "If your system supports RFC 8905, you can do this by opening this URI:": [
+ "Si su sistema soporta RFC 8905, puede hacerlo abriendo este URI:"
+ ],
+ "it should be greater than 0": [
+ "debe ser mayor que 0"
+ ],
+ "must be a valid URL": [
+ "debe ser una URL válida"
+ ],
+ "Initial balance": [
+ "Balance inicial"
+ ],
+ "balance prior to deposit": [
+ "saldo antes del depósito"
+ ],
+ "Exchange URL": [
+ "URL del proveedor"
+ ],
+ "URL of exchange": [
+ "URL del proveedor"
+ ],
+ "Next": [
+ "Siguiente"
+ ],
+ "Wire method": [
+ "Método de transferencia"
+ ],
+ "method to use for wire transfer": [
+ "método a usar para realizar la transferencia bancaria"
+ ],
+ "Select one wire method": [
+ "Selecciona un método de transferencia"
+ ],
+ "could not create reserve": [
+ "no se pudo crear la reserva"
+ ],
+ "Valid until": [
+ "Válido hasta"
+ ],
+ "Created balance": [
+ "Balance creado"
+ ],
+ "Exchange balance": [
+ "Balance del proveedor"
+ ],
+ "Picked up": [
+ "Recogido"
+ ],
+ "Committed": [
+ "Comiteado"
+ ],
+ "Account address": [
+ "Dirección de cuenta"
+ ],
+ "Subject": [
+ "Asunto"
+ ],
+ "Tips": [
+ "Propinas"
+ ],
+ "No tips has been authorized from this reserve": [
+ "No se han autorizado propinas de esta reserva"
+ ],
+ "Authorized": [
+ "Autorizado"
+ ],
+ "Expiration": [
+ "Expiración"
+ ],
+ "amount of tip": [
+ "monto"
+ ],
+ "Justification": [
+ "Jurisdicción"
+ ],
+ "reason for the tip": [
+ "motivo de la propina"
+ ],
+ "URL after tip": [
+ "URL después de la recompensa"
+ ],
+ "URL to visit after tip payment": [
+ "URL para visitar después del pago de la propina"
+ ],
+ "Reserves not yet funded": [
+ "Servidor no encontrado"
+ ],
+ "Reserves ready": [
+ "Reservas listas"
+ ],
+ "add new reserve": [
+ "cargar nuevas transferencias"
+ ],
+ "Expires at": [
+ "Vence en"
+ ],
+ "Initial": [
+ "Inicial"
+ ],
+ "delete selected reserve from the database": [
+ "eliminar la reserva seleccionada de la base de datos"
+ ],
+ "authorize new tip from selected reserve": [
+ "autorizar nueva punta de reserva seleccionada"
+ ],
+ "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": [
+ "demasiado corta"
+ ],
+ "just letters and numbers from 2 to 7": [
+ "sólo letras y números del 2 al 7"
+ ],
+ "size of the key should be 32": [
+ "el tamaño de la clave debe ser 32"
+ ],
+ "Identifier": [
+ "Identificador"
+ ],
+ "Name of the template in URLs.": [
+ "Nombre de la plantilla en las URL."
+ ],
+ "Describe what this template stands for": [
+ "Describa lo que representa esta plantilla"
+ ],
+ "Fixed summary": [
+ "Estado de orden"
+ ],
+ "If specified, this template will create order with the same summary": [
+ "Si se especifica, esta plantilla creará pedidos con el mismo resumen"
+ ],
+ "Fixed price": [
+ "precio unitario"
+ ],
+ "If specified, this template will create order with the same price": [
+ "Si se especifica, esta plantilla creará pedidos con el mismo precio"
+ ],
+ "Minimum age": [
+ "Edad mínima"
+ ],
+ "Is this contract restricted to some age?": [
+ "¿Este contrato está restringido a alguna edad?"
+ ],
+ "Payment timeout": [
+ "Opciones de pago"
+ ],
+ "How much time has the customer to complete the payment once the order was created.": [
+ "Cuánto tiempo tiene el cliente para completar el pago una vez creado el pedido."
+ ],
+ "Verification algorithm": [
+ "Algoritmo de verificación"
+ ],
+ "Algorithm to use to verify transaction in offline mode": [
+ "Algoritmo a utilizar para verificar la transacción en modo offline"
+ ],
+ "Point-of-sale key": [
+ "Clave punto de venta"
+ ],
+ "Useful to validate the purchase": [
+ "Útil para validar la compra"
+ ],
+ "generate random secret key": [
+ "generar clave secreta aleatoria"
+ ],
+ "random": [
+ "aleatorio"
+ ],
+ "show secret key": [
+ "mostrar clave secreta"
+ ],
+ "hide secret key": [
+ "ocultar clave secreta"
+ ],
+ "hide": [
+ "ocultar"
+ ],
+ "show": [
+ "mostrar"
+ ],
+ "could not inform template": [
+ "no se pudo informar la transferencia"
+ ],
+ "Amount is required": [
+ "Login necesario"
+ ],
+ "Order summary is required": [
+ "Se requiere resumen del pedido"
+ ],
+ "New order for template": [
+ "cargar viejas transferencias"
+ ],
+ "Amount of the order": [
+ "Importe del pedido"
+ ],
+ "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.": [
+ "Aquí puede especificar un valor por defecto para los campos que no son fijos. Los valores por defecto pueden ser editados por el cliente antes del pago."
+ ],
+ "Fixed amount": [
+ "Importe fijo"
+ ],
+ "Default amount": [
+ "Importe por defecto"
+ ],
+ "Default summary": [
+ "Estado de orden"
+ ],
+ "Print": [
+ "Imprimir"
+ ],
+ "Setup TOTP": [
+ "Configurar TOTP"
+ ],
+ "Templates": [
+ "Plantillas"
+ ],
+ "add new templates": [
+ "añadir nuevas plantillas"
+ ],
+ "load more templates before the first one": [
+ "cargar más plantillas antes de la primera"
+ ],
+ "load newer templates": [
+ "cargar nuevas transferencias"
+ ],
+ "delete selected templates from the database": [
+ "eliminar las plantillas seleccionadas de la base de datos"
+ ],
+ "use template to create new order": [
+ "utilizar la plantilla para crear un nuevo pedido"
+ ],
+ "create qr code for the template": [
+ "No se pudo create el reembolso"
+ ],
+ "load more templates after the last one": [
+ "cargar más plantillas después de la última"
+ ],
+ "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": [
+ "ID de webhook a utilizar"
+ ],
+ "Event": [
+ "Evento"
+ ],
+ "The event of the webhook: why the webhook is used": [
+ "El evento del webhook: por qué se utiliza el webhook"
+ ],
+ "Method": [
+ "Método"
+ ],
+ "Method used by the webhook": [
+ "Método utilizado por el webhook"
+ ],
+ "URL": [
+ "URL"
+ ],
+ "URL of the webhook where the customer will be redirected": [
+ "URL del webhook al que se redirigirá al cliente"
+ ],
+ "Header": [
+ "Cabecera"
+ ],
+ "Header template of the webhook": [
+ "Plantilla de cabecera del webhook"
+ ],
+ "Body": [
+ "Cuerpo"
+ ],
+ "Body template by the webhook": [
+ "Plantilla del cuerpo del webhook"
+ ],
+ "Webhooks": [
+ "Webhooks"
+ ],
+ "add new webhooks": [
+ "añadir nuevos webhooks"
+ ],
+ "load more webhooks before the first one": [
+ "cargar más webhooks antes del primero"
+ ],
+ "load newer webhooks": [
+ "cargar nuevas ordenes"
+ ],
+ "Event type": [
+ "Tipo de evento"
+ ],
+ "delete selected webhook from the database": [
+ "eliminar el webhook seleccionado de la base de datos"
+ ],
+ "load more webhooks after the last one": [
+ "cargar más webhooks después del último"
+ ],
+ "load older webhooks": [
+ "cargar webhooks antiguos"
+ ],
+ "There is no webhooks yet, add more pressing the + sign": [
+ "No hay webhooks todavía, añade más pulsando sobre el símbolo +"
+ ],
+ "webhook delete successfully": [
+ "el webhook ha sido borrado correctamente"
+ ],
+ "could not delete the webhook": [
+ "no se ha podido eliminar el webhook"
+ ],
+ "check the id, does not look valid": [
+ "comprueba el ID, parece no ser 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": [
+ "Abono en cuenta bancaria"
+ ],
+ "Select one account": [
+ "Selecciona una cuenta"
+ ],
+ "Bank account of the merchant where the payment was received": [
+ "Cuenta bancaria del comerciante donde se recibió el pago"
+ ],
+ "Wire transfer ID": [
+ "ID de la transferencia"
+ ],
+ "unique identifier of the wire transfer used by the exchange, must be 52 characters long": [
+ "identificador único de la transferencia utilizado por el proveedor, debe tener 52 caracteres"
+ ],
+ "Base URL of the exchange that made the transfer, should have been in the wire transfer subject": [
+ "URL base del proveedor que realizó la transferencia, debería haber estado en el asunto de la transferencia bancaria"
+ ],
+ "Amount credited": [
+ "Monto abonado"
+ ],
+ "Actual amount that was wired to the merchant's bank account": [
+ "Monto real que se transfirió a la cuenta bancaria del comerciante"
+ ],
+ "could not inform transfer": [
+ "no se pudo informar la transferencia"
+ ],
+ "Transfers": [
+ "Transferencias"
+ ],
+ "add new transfer": [
+ "añadir nueva transferencia"
+ ],
+ "load more transfers before the first one": [
+ "cargar más transferencias antes de la primera"
+ ],
+ "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": [
+ "filtrar por dirección de cuenta"
+ ],
+ "only show wire transfers confirmed by the merchant": [
+ "mostrar sólo las transferencias confirmadas por el comerciante"
+ ],
+ "only show wire transfers claimed by the exchange": [
+ "sólo muestran las transferencias reclamadas por el proveedor"
+ ],
+ "Unverified": [
+ "Sin verificar"
+ ],
+ "is not valid": [
+ "no es válido"
+ ],
+ "is not a number": [
+ "no es un número"
+ ],
+ "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": [
+ "Los números IBAN usualmente tienen mas de 4 digitos"
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Los números IBAN usualmente tienen menos de 34 digitos"
+ ],
+ "IBAN country code not found": [
+ "Código de pais de IBAN no encontrado"
+ ],
+ "IBAN number is not valid, checksum is wrong": [
+ "El número IBAN no es válido, falló la verificación"
+ ],
+ "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 la cuenta"
+ ],
+ "Business Identifier Code.": [
+ "Código de identificación de la empresa."
+ ],
+ "Bank Account Number.": [
+ "Número de cuenta bancaria."
+ ],
+ "Unified Payment Interface.": [
+ "Interfaz de pago unificado."
+ ],
+ "Bitcoin protocol.": [
+ "Protocolo Bitcoin."
+ ],
+ "Ethereum protocol.": [
+ "Protocolo Ethereum."
+ ],
+ "Interledger protocol.": [
+ "Protocolo Interledger."
+ ],
+ "Host": [
+ "Host"
+ ],
+ "Bank host.": [
+ "Host del banco."
+ ],
+ "Bank account.": [
+ "Cuenta bancaria."
+ ],
+ "Bank account owner's name.": [
+ "Nombre del titular de la cuenta bancaria."
+ ],
+ "No accounts yet.": [
+ "Aún no hay cuentas."
+ ],
+ "Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.": [
+ "Nombre de la instancia en URL. La instancia \"por defecto\" es especial, ya que se utiliza para administrar otras instancias."
+ ],
+ "Business name": [
+ "Nombre del negocio"
+ ],
+ "Legal name of the business represented by this instance.": [
+ "Nombre legal de la empresa representada por esta instancia."
+ ],
+ "Email": [
+ "Correo eletrónico"
+ ],
+ "Contact email": [
+ "Correo electrónico del contacto"
+ ],
+ "Website URL": [
+ "URL de sitio web"
+ ],
+ "URL.": [
+ "URL."
+ ],
+ "Logo": [
+ "Logotipo"
+ ],
+ "Logo image.": [
+ "Imagen del logotipo."
+ ],
+ "Bank account": [
+ "Cuenta bancaria"
+ ],
+ "URI specifying bank account for crediting revenue.": [
+ "URI que especifica la cuenta bancaria para acreditar los ingresos."
+ ],
+ "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.": [
+ "Comisiones de depósito máximas que este comerciante está dispuesto a pagar por pedido por defecto."
+ ],
+ "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.": [
+ "Comisiones de transferencia máximas que este comerciante está dispuesto a pagar por transferencia por defecto."
+ ],
+ "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.": [
+ "El número de pedidos que excedan las tarifas de transferencia bancaria se dividirá para calcular el recargo por pedido."
+ ],
+ "Physical location of the merchant.": [
+ "Ubicación física del comerciante."
+ ],
+ "Jurisdiction": [
+ "Jurisdicción"
+ ],
+ "Jurisdiction for legal disputes with the merchant.": [
+ "Jurisdicción para disputas legales con el comerciante."
+ ],
+ "Default payment delay": [
+ "Retraso del pago por defecto"
+ ],
+ "Time customers have to pay an order before the offer expires by default.": [
+ "Tiempo que los clientes tienen para pagar un pedido antes de que caduque la oferta de forma predeterminada."
+ ],
+ "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.": [
+ "Tiempo máximo que un proveedor puede retrasar la transferencia de fondos al comerciante, lo que le permite agrupar pagos más pequeños en transferencias más grandes y reducir las comisiones por transferencia."
+ ],
+ "Instance id": [
+ "ID de instancia"
+ ],
+ "Change the authorization method use for this instance.": [
+ "Cambiar el método de autorización a usar para esta 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.": [
+ "Por favor, introduzca su clave de acceso."
+ ],
+ "Access Token": [
+ "Token de acceso"
+ ],
+ "The request to the backend take too long and was cancelled": [
+ "La petición al backend tardó demasiado y fue cancelada"
+ ],
+ "Diagnostic from %1$s is \"%2$s\"": [
+ "El Diagnóstico de %1$s es \"%2$s\""
+ ],
+ "The backend reported a problem: HTTP status #%1$s": [
+ "El backend ha informado de un problema: HTTP status #%1$s"
+ ],
+ "Diagnostic from %1$s is '%2$s'": [
+ "El Diagnóstico de %1$s es '%2$s'"
+ ],
+ "Access denied": [
+ "Acceso denegado"
+ ],
+ "The access token provided is invalid.": [
+ "El token de acceso proporcionado no es válido."
+ ],
+ "No 'default' instance configured yet.": [
+ "No se ha configurado una instancia por 'defecto' todavía"
+ ],
+ "Create a 'default' instance to begin using the merchant backoffice.": [
+ "Cree una instancia \"por defecto\" para empezar a utilizar el backoffice comerciante."
+ ],
+ "The access token provided is invalid": [
+ "El token de acceso proporcionado no es válido"
+ ],
+ "Hide for today": [
+ "Ocultar por hoy"
+ ],
+ "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": [
+ "El servidor responde con un código de error"
+ ],
+ "Got message %1$s from %2$s": [
+ "Recibimos el mensaje %1$s desde %2$s"
+ ],
+ "Response from server is unreadable, http status: %1$s": [
+ "La respuesta del servidor es ilegible, estado http: %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"
+ ]
+ }
+ },
"domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "es",
+ "completeness": 100
+};
+
+strings['en'] = {
"locale_data": {
"messages": {
"": {
@@ -8044,44 +9664,47 @@ strings['it'] = {
""
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=(n != 1);",
+ "lang": "",
+ "completeness": 0
};
-strings['sv'] = {
- "domain": "messages",
+strings['de'] = {
"locale_data": {
"messages": {
"": {
"domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "de"
},
"Cancel": [
- ""
+ "Zurück"
],
"%1$s": [
- ""
+ "%1$s"
],
"Close": [
- ""
+ "Schließen"
],
"Continue": [
- ""
+ "Weiter"
],
"Clear": [
- ""
+ "Leeren"
],
"Confirm": [
- ""
+ "Bestätigen"
],
"is not the same as the current access token": [
""
],
"cannot be empty": [
- ""
+ "darf nicht leer sein"
],
"cannot be the same as the old token": [
- ""
+ "muss sich vom alten Token unterscheiden"
],
"is not the same": [
""
@@ -8090,19 +9713,19 @@ strings['sv'] = {
""
],
"Old access token": [
- ""
+ "Altes Zugriffstoken"
],
"access token currently in use": [
""
],
"New access token": [
- ""
+ "Neues Zugriffstoken"
],
"next access token to be used": [
""
],
"Repeat access token": [
- ""
+ "Zugriffstoken wiederholen"
],
"confirm the same access token": [
""
@@ -8132,22 +9755,22 @@ strings['sv'] = {
""
],
"Instances": [
- ""
+ "Instanzen"
],
"Delete": [
- ""
+ "Löschen"
],
"add new instance": [
- ""
+ "neue Instanz hinzufügen"
],
"ID": [
""
],
"Name": [
- ""
+ "Name"
],
"Edit": [
- ""
+ "Bearbeiten"
],
"Purge": [
""
@@ -8300,7 +9923,7 @@ strings['sv'] = {
""
],
"Amount": [
- ""
+ "Betrag"
],
"Taxes can be in currencies that differ from the main currency used by the merchant.": [
""
@@ -8363,94 +9986,94 @@ strings['sv'] = {
""
],
"required": [
- ""
+ "erforderlich"
],
"not valid": [
- ""
+ "nicht gültig"
],
"must be greater than 0": [
""
],
"not a valid json": [
- ""
+ "kein gültiges JSON-Format"
],
"should be in the future": [
- ""
+ "sollte in der Zukunft liegen"
],
"refund deadline cannot be before pay deadline": [
- ""
+ "Die Rückerstattungsfrist kann nicht vor der Zahlungsfrist liegen"
],
"wire transfer deadline cannot be before refund deadline": [
- ""
+ "Die Überweisungsfrist kann nicht vor der Rückerstattungsfrist liegen"
],
"wire transfer deadline cannot be before pay deadline": [
- ""
+ "Die Überweisungsfrist kann nicht vor der Zahlungsfrist liegen"
],
"should have a refund deadline": [
- ""
+ "sollte eine Rückerstattungsfrist haben"
],
"auto refund cannot be after refund deadline": [
- ""
+ "Die automatische Rückerstattung kann nicht nach der Rückerstattungsfrist erfolgen"
],
"Manage products in order": [
- ""
+ "Artikel in der Bestellung verwalten"
],
"Manage list of products in the order.": [
- ""
+ "Liste der Artikel in der Bestellung verwalten."
],
"Remove this product from the order.": [
- ""
+ "Diesen Artikel aus der Bestellung entfernen."
],
"Total price": [
- ""
+ "Gesamtpreis"
],
"total product price added up": [
""
],
"Amount to be paid by the customer": [
- ""
+ "Zu zahlender Betrag"
],
"Order price": [
- ""
+ "Bestellsumme"
],
"final order price": [
""
],
"Summary": [
- ""
+ "Zusammenfassung"
],
"Title of the order to be shown to the customer": [
- ""
+ "Bezeichnung der Bestellung, die den Kunden angezeigt wird"
],
"Shipping and Fulfillment": [
""
],
"Delivery date": [
- ""
+ "Lieferdatum"
],
"Deadline for physical delivery assured by the merchant.": [
- ""
+ "Vom Händler zugesicherte Zustellfrist."
],
"Location": [
""
],
"address where the products will be delivered": [
- ""
+ "Zustelladresse der Artikel"
],
"Fulfillment URL": [
- ""
+ "Adresse digitaler Dienstleistung (Fulfillment-URL)"
],
"URL to which the user will be redirected after successful payment.": [
- ""
+ "URL der von Kunden zu besuchenden Adresse nach erfolgter Bezahlung."
],
"Taler payment options": [
- ""
+ "Taler-Zahlungsoptionen"
],
"Override default Taler payment settings for this order": [
""
],
"Payment deadline": [
- ""
+ "Zahlungsfrist"
],
"Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.": [
""
@@ -8558,7 +10181,7 @@ strings['sv'] = {
""
],
"Date": [
- ""
+ "Datum"
],
"Refund": [
""
@@ -8744,7 +10367,7 @@ strings['sv'] = {
""
],
"Back": [
- ""
+ "Zurück"
],
"refund created successfully": [
""
@@ -8774,7 +10397,7 @@ strings['sv'] = {
""
],
"Refunded": [
- ""
+ "Rückerstattet"
],
"only show orders where customers paid, but wire payments from payment provider are still pending": [
""
@@ -8957,10 +10580,10 @@ strings['sv'] = {
""
],
"Exchange URL": [
- ""
+ "URL des Exchange"
],
"URL of exchange": [
- ""
+ "URL des Exchange"
],
"Next": [
""
@@ -8996,7 +10619,7 @@ strings['sv'] = {
""
],
"Subject": [
- ""
+ "Verwendungszweck"
],
"Tips": [
""
@@ -9401,16 +11024,16 @@ strings['sv'] = {
""
],
"IBAN numbers usually have more that 4 digits": [
- ""
+ "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
],
"IBAN numbers usually have less that 34 digits": [
- ""
+ "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
],
"IBAN country code not found": [
- ""
+ "IBAN-Ländercode wurde nicht gefunden"
],
"IBAN number is not valid, checksum is wrong": [
- ""
+ "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
],
"Target type": [
""
@@ -9650,6 +11273,10 @@ strings['sv'] = {
""
]
}
- }
+ },
+ "domain": "messages",
+ "plural_forms": "nplurals=2; plural=n != 1;",
+ "lang": "de",
+ "completeness": 9
};
diff --git a/packages/merchant-backoffice-ui/src/i18n/sv.po b/packages/merchant-backoffice-ui/src/i18n/sv.po
index d8d0bae29..9fa25de36 100644
--- a/packages/merchant-backoffice-ui/src/i18n/sv.po
+++ b/packages/merchant-backoffice-ui/src/i18n/sv.po
@@ -27,223 +27,815 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/components/modal/index.tsx:71
+#: src/components/ErrorLoadingMerchant.tsx:45
#, c-format
-msgid "Cancel"
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
msgstr ""
#: src/components/modal/index.tsx:79
#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:87
+#, c-format
msgid "%1$s"
msgstr ""
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr ""
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr ""
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr ""
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
#, c-format
-msgid "is not the same as the current access token"
+msgid "Required"
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:248
#, c-format
-msgid "cannot be empty"
+msgid "Letter must be a JSON string"
msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:250
#, c-format
-msgid "cannot be the same as the old token"
+msgid "JSON string is invalid"
msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:255
#, c-format
-msgid "is not the same"
+msgid "Import"
+msgstr ""
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr ""
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr ""
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr ""
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr ""
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, c-format
+msgid "Account id"
+msgstr ""
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, c-format
+msgid "Is not the same as the current access token"
+msgstr ""
+
+#: src/components/modal/index.tsx:542
+#, c-format
+msgid "Can't be the same as the old token"
+msgstr ""
+
+#: src/components/modal/index.tsx:546
+#, c-format
+msgid "Is not the same"
+msgstr ""
+
+#: src/components/modal/index.tsx:554
#, c-format
msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "access token currently in use"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, c-format
msgid "New access token"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:578
#, c-format
-msgid "next access token to be used"
+msgid "Next access token to be used"
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, c-format
msgid "Repeat access token"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:584
#, c-format
-msgid "confirm the same access token"
+msgid "Confirm the same access token"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, c-format
msgid "Clearing the access token will mean public access to the instance"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:616
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Can't be the same as the old access token"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid ""
"With external authorization method no check will be done by the merchant "
"backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, c-format
+msgid "Your instance not found"
+msgstr ""
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/paths/login/index.tsx:95
+#, c-format
+msgid "Please enter your access token for %1$s."
+msgstr ""
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/admin/list/TableActive.tsx:100
#, c-format
-msgid "add new instance"
+msgid "Add new instance"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, c-format
+msgid "This is not a valid host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, c-format
+msgid "Invalid url"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, c-format
+msgid "Account:"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, c-format
+msgid "Do not change"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -266,57 +858,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -359,66 +1001,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
-#, c-format
-msgid "Product"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "search products by it's description or id"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:151
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "no products found with that description"
+msgid "no match found with that description or id"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, c-format
+msgid "Search product"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:113
#, c-format
-msgid "how many products will be added"
+msgid "How many products will be added"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -431,54 +1068,74 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, c-format
+msgid "Invalid"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr ""
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -486,12 +1143,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -501,7 +1158,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -511,2213 +1168,2399 @@ 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"
+msgid "Amount in the current currency."
msgstr ""
-#: src/components/product/ProductList.tsx:41
+#: src/components/product/NonInventoryProductForm.tsx:208
#, c-format
-msgid "description"
+msgid "How many products will be added."
msgstr ""
-#: src/components/product/ProductList.tsx:44
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "quantity"
+msgid "Taxes"
msgstr ""
-#: src/components/product/ProductList.tsx:47
+#: src/components/product/ProductList.tsx:46
#, c-format
-msgid "unit price"
+msgid "Unit price"
msgstr ""
-#: src/components/product/ProductList.tsx:50
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "total price"
+msgid "Total price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:153
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "required"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "not valid"
+msgid "Refund deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "must be greater than 0"
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
+#: src/paths/instance/orders/create/CreatePage.tsx:188
#, c-format
-msgid "not a valid json"
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:170
+#: src/paths/instance/orders/create/CreatePage.tsx:196
#, c-format
-msgid "should be in the future"
+msgid "Must have a refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:173
+#: src/paths/instance/orders/create/CreatePage.tsx:201
#, c-format
-msgid "refund deadline cannot be before pay deadline"
+msgid "Auto refund can't be after refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:179
+#: src/paths/instance/orders/create/CreatePage.tsx:208
#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+msgid "Must be in the future"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:190
+#: src/paths/instance/orders/create/CreatePage.tsx:376
#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+msgid "Simple"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:197
+#: src/paths/instance/orders/create/CreatePage.tsx:388
#, c-format
-msgid "should have a refund deadline"
+msgid "Advanced"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:202
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
-msgid "auto refund cannot be after refund deadline"
+msgid "Manage products in order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:360
+#: src/paths/instance/orders/create/CreatePage.tsx:404
#, c-format
-msgid "Manage products in order"
+msgid "%1$s products with a total price of %2$s."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:437
+#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
-msgid "final order price"
+msgid "Final order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "address where the products will be delivered"
+msgid "Address where the products will be delivered"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, c-format
-msgid "Payment deadline"
+msgid "Payment time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, 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"
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:492
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, c-format
-msgid "Deadline for the exchange to make the wire transfer."
+msgid "Default"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:561
#, c-format
-msgid "Auto-refund deadline"
+msgid "Refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
msgid ""
-"Time until which the wallet will automatically check for refunds without "
-"user interaction."
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:502
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Maximum deposit fee"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, 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."
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:507
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Maximum wire fee"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Wire fee amortization"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with "
-"enough entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid "Custom information to be included in the contract for this order."
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:541
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, c-format
-msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgid "Additional information"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:55
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
-msgid "days"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:65
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "hours"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "minutes"
+msgid "Custom field name"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "seconds"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/create/CreatePage.tsx:796
#, c-format
-msgid "forever"
+msgid "No deadline"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sM"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "%1$sY"
+msgid "Could not create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$sd"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$sh"
+msgid "No more stock for product with id \"%1$s\"."
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
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "create order"
+msgid "Create order"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
+msgid "Load first page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, c-format
msgid "copy url"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:225
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "load older orders"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:242
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "No orders have been found matching your query!"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:288
-#, c-format
-msgid "duplicated"
+msgid "Load next page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:299
+#: src/paths/instance/orders/list/Table.tsx:233
#, 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"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "amount"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "reason"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "amount to be refunded"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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
+#: src/paths/instance/orders/list/Table.tsx:391
#, c-format
-msgid "requested by the customer"
+msgid "Requested by the customer"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "other"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "why this order is being refunded"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "more information to give context"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "total price for the transaction"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "Max wire fee"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize "
-"wire fees on average"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Created at"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "time when this contract was generated"
+msgid "Payment deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:116
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid ""
-"after this deadline, the merchant won't accept payments for the contract"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:122
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, c-format
-msgid "where the order will be delivered"
+msgid "Where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
msgid ""
-"how long the wallet should try to get an automatic refund for the purchase"
+"How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "claimed"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "claimed at"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/details/DetailPage.tsx:465
#, c-format
-msgid "wired"
+msgid "Wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/details/DetailPage.tsx:490
#, c-format
-msgid "refund order"
+msgid "Refund order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "not refundable"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, c-format
msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:636
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "unpaid"
+msgid "Unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:654
+#: src/paths/instance/orders/details/DetailPage.tsx:659
#, c-format
-msgid "pay at"
+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
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/orders/details/DetailPage.tsx:716
#, c-format
msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid ""
"Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, c-format
msgid "Back"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:79
+#: src/paths/instance/orders/details/index.tsx:88
#, c-format
-msgid "refund created successfully"
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/details/index.tsx:85
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "could not create the refund"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "select date to show nearby orders"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "order id"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "remove all filters"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "only show paid orders"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "Paid"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:142
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "only show orders with refunds"
+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
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
msgid ""
-"only show orders where customers paid, but wire payments from payment "
+"Only show orders where customers paid, but wire payments from payment "
"provider are still pending"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "clear date filter"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "Enter an order id"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "order not found"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "could not get the order to refund"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/orders/list/index.tsx:114
#, c-format
-msgid "Loading..."
+msgid "Order id"
msgstr ""
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid ""
-"click here to configure the stock of the product, leave it as is and the "
-"backend will not control stock"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/components/form/InputStock.tsx:109
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Manage stock"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/components/form/InputStock.tsx:115
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "this product has been configured without stock control"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/components/form/InputStock.tsx:119
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "Infinite"
+msgid "Internal id on the system"
msgstr ""
-#: src/components/form/InputStock.tsx:136
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Incoming"
+msgid "Verification algorithm"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Lost"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Current"
+msgid "Device key"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid "remove stock control for this product"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "without stock"
+msgid "Your device need to have exactly the same value"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "Next restock"
+msgid "Generate random secret key"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "Delivery address"
+msgid "Random"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "illustration of the product for customers"
+msgid "Device added successfully"
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "product description for customers"
+msgid "Could not add device"
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "Age restricted"
+msgid "OTP Devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/paths/instance/otp_devices/list/Table.tsx:62
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Add new devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
-"items, 5 meters) for customers"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid ""
-"sale price for customers, including taxes, for above units of the product"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "Stock"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid ""
-"product inventory for products with finite supply (for internal use only)"
+msgid "There is no devices yet, add more pressing the + sign"
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Need to complete marked fields"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "could not create product"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "Products"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:73
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "add product to inventory"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "Sell"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "Profit"
+msgid "Template id is unknown"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Sold"
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "free"
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the "
+"backend will not control stock."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "go to product update page"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "Update"
+msgid "This product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "remove this product from the database"
+msgid "Infinite"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "update the product with new price"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/components/form/InputStock.tsx:169
#, c-format
-msgid "update product with new price"
+msgid "Incoming"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/components/form/InputStock.tsx:170
#, c-format
-msgid "add more elements to the inventory"
+msgid "Lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/components/form/InputStock.tsx:185
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Current"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "new price for the product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/components/form/InputStock.tsx:195
#, c-format
-msgid "the are value with errors"
+msgid "without stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/components/form/InputStock.tsx:204
#, c-format
-msgid "update product with new stock and price"
+msgid "Next restock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "There is no products yet, add more pressing the + sign"
+msgid "Warehouse address"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:86
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product updated successfully"
+msgid "Add element to the list"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/components/product/ProductForm.tsx:120
#, c-format
-msgid "could not update the product"
+msgid "Invalid amount"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "product delete successfully"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/paths/instance/products/list/index.tsx:109
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "could not delete the product"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Product id:"
+msgid "Product description for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/components/product/ProductForm.tsx:197
#, 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."
+msgid "Age restriction"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/components/product/ProductForm.tsx:198
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Is this product restricted for customer below certain age?"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/components/product/ProductForm.tsx:199
#, c-format
-msgid "it should be greater than 0"
+msgid "Minimum age of the customer"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/components/product/ProductForm.tsx:203
#, c-format
-msgid "must be a valid URL"
+msgid "Unit name"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/components/product/ProductForm.tsx:204
#, c-format
-msgid "Initial balance"
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "balance prior to deposit"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid "Exchange URL"
+msgid "Price per unit"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/components/product/ProductForm.tsx:210
#, c-format
-msgid "URL of exchange"
+msgid ""
+"Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/components/product/ProductForm.tsx:214
#, c-format
-msgid "Next"
+msgid "Stock"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid "Wire method"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "method to use for wire transfer"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Select one wire method"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/components/product/ProductForm.tsx:231
#, c-format
-msgid "could not create reserve"
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/components/product/ProductForm.tsx:232
#, c-format
-msgid "Valid until"
+msgid "Categories where this product will be listed on."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "Created balance"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Exchange balance"
+msgid "Could not create product"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "Picked up"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Committed"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Account address"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
-msgid "Subject"
+msgid "Sold"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "Tips"
+msgid "Free"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
-msgid "Authorized"
+msgid "Update"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/products/list/Table.tsx:283
#, c-format
-msgid "Expiration"
+msgid "Remove this product from the database"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "amount of tip"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "Justification"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/products/list/Table.tsx:373
#, c-format
-msgid "reason for the tip"
+msgid "Update product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/products/list/Table.tsx:384
#, c-format
-msgid "URL after tip"
+msgid "Confirm update"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "Reserves not yet funded"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "Reserves ready"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "add new reserve"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "Expires at"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "Initial"
+msgid "There is no products yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid ""
-"There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Product updated successfully"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid "Expected Balance"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "could not create the tip"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/products/list/index.tsx:149
#, c-format
-msgid "should not be empty"
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "should be greater that 0"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/products/list/index.tsx:173
#, c-format
-msgid "can't be empty"
+msgid "Deleting an product can't be undone."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:85
+#, c-format
+msgid "Product (ID: %1$s) has been updated"
+msgstr ""
+
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "to short"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "Invalid. only characters and numbers"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:112
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Must be greater that 0"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/create/CreatePage.tsx:119
#, c-format
-msgid "size of the key should be 32"
+msgid "To short"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
msgid "Identifier"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Fixed summary"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Fixed price"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, c-format
+msgid "Amount is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
msgid "Minimum age"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, 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
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Verification algorithm"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "Point-of-sale key"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Useful to validate the purchase"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "generate random secret key"
+msgid "No device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "random"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "show secret key"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "hide secret key"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "hide"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/paths/instance/templates/list/Table.tsx:66
#, c-format
-msgid "show"
+msgid "Add new templates"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "could not inform template"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "Amount is required"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid "Order summary is required"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "New order for template"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "Amount of the order"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "Order summary"
+msgid "Load more templates after the last one"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/templates/list/Table.tsx:214
#, c-format
-msgid "could not create order from template"
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/instance/templates/list/index.tsx:91
#, 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."
+msgid "Jump to template with the given template ID"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:92
+#, c-format
+msgid "Template identification"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:132
+#, c-format
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/paths/instance/templates/list/index.tsx:137
#, c-format
-msgid "Fixed amount"
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Default amount"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/templates/list/index.tsx:160
#, c-format
-msgid "Default summary"
+msgid "Deleting an template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/templates/list/index.tsx:162
+#, c-format
+msgid "can't be undone"
+msgstr ""
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
msgid "Print"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Setup TOTP"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "Templates"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "add new templates"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "load more templates before the first one"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "load newer templates"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "delete selected templates from the database"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "use template to create new order"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid "create qr code for the template"
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "load more templates after the last one"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "load older templates"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/token/DetailPage.tsx:121
#, c-format
-msgid "could not delete the template"
+msgid "Current access token"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "could not update template"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/token/DetailPage.tsx:142
#, c-format
-msgid "should be one of '%1$s'"
+msgid "Clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/token/DetailPage.tsx:177
#, c-format
-msgid "Webhook ID to use"
+msgid "Confirm change"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "Event"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "Method"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "Method used by the webhook"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "URL"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "Header"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Header template of the webhook"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "Body"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Body template by the webhook"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Webhooks"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "add new webhooks"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Duration"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "load newer webhooks"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "Event type"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "delete selected webhook from the database"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "load older webhooks"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "Go to token family update page"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
#, c-format
-msgid "webhook delete successfully"
+msgid "Remove this token family from the database"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
#, c-format
-msgid "could not delete the webhook"
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
#, c-format
-msgid "check the id, does not look valid"
+msgid "Token family updated successfully"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Could not update the token family"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, c-format
+msgid "Failed to delete token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
+#, c-format
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, c-format
+msgid "Token familty updated successfully"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
+#, c-format
+msgid "Could not update token family"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
+#, c-format
+msgid "Check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
+#, c-format
+msgid "Must have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
#, 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
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
msgid "Amount credited"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "could not inform transfer"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Transfers"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "add new transfer"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/transfers/list/Table.tsx:64
#, c-format
-msgid "load more transfers before the first one"
+msgid "Add new transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "load newer transfers"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "unknown"
+msgid "never"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "delete selected transfer from the database"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "load more transfer after the last one"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "load older transfers"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/paths/instance/transfers/list/ListPage.tsx:76
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:83
#, c-format
-msgid "filter by account address"
+msgid "All accounts"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/paths/instance/transfers/list/ListPage.tsx:105
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Only show wire transfers confirmed by the merchant"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/paths/instance/transfers/list/ListPage.tsx:115
+#, c-format
+msgid "Only show wire transfers claimed by the exchange"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
msgid "Unverified"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid "is not valid"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid "is not a number"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "must be 1 or greater"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "max 7 lines"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "change authorization configuration"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+msgid "Doesn't match"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:82
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Enable access control"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "IBAN country code not found"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Target type"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Business name"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "Routing"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "Routing number."
+msgid "Email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "Account"
+msgid "Contact email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "Account number."
+msgid "Website URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "Business Identifier Code."
+msgid "URL."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "Bank Account Number."
+msgid "Logo"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "Unified Payment Interface."
+msgid "Logo image."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "Bitcoin protocol."
+msgid "Physical location of the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "Ethereum protocol."
+msgid "Jurisdiction"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Interledger protocol."
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "Host"
+msgid "Pay transaction fee"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "Bank host."
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Bank account."
+msgid "Default payment delay"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "Bank account owner's name."
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "No accounts yet."
+msgid "Default wire transfer delay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it "
-"is used to administer other instances."
+"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/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "Business name"
+msgid "Instance id"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/update/index.tsx:108
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Failed to update instance"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "Email"
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "Contact email"
+msgid "Must be one of '%1$s'"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "Website URL"
+msgid "Webhook ID to use"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "URL."
+msgid "Event"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "Logo"
+msgid "Pay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Logo image."
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Bank account"
+msgid "Method"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "GET"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Default max deposit fee"
+msgid "POST"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid ""
-"Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "PUT"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
-msgid "Default max wire fee"
+msgid "PATCH"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid ""
-"Maximum wire fees this merchant is willing to pay per wire transfer by "
-"default."
+msgid "HEAD"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Default wire fee amortization"
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Physical location of the merchant."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Jurisdiction"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "order's description"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
#, c-format
-msgid "Default payment delay"
+msgid "order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid ""
-"Time customers have to pay an order before the offer expires by default."
+msgid "order's unique identification"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Default wire transfer delay"
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, 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."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "Instance id"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Http body"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Manage access token"
+msgid "Body template by the webhook"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Failed to create instance"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Login required"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Please enter your access token."
+msgid "Could not create webhook"
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "Access Token"
+msgid "Webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "Add new webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Event type"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Access denied"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid "The access token provided is invalid."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Webhook delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "The access token provided is invalid"
+msgid "Header"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid "Hide for today"
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Instance"
+msgid "Body"
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Settings"
+msgid "Webhook updated"
msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Connection"
+msgid "Could not update webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "New"
+msgid "Language"
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "List"
+msgid "Set default"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Log out"
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, c-format
+msgid "Date format"
+msgstr ""
+
+#: src/paths/settings/index.tsx:131
+#, c-format
+msgid "How the date is going to be displayed"
+msgstr ""
+
+#: src/paths/settings/index.tsx:134
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/paths/settings/index.tsx:135
+#, c-format
+msgid ""
+"Shows more options and tools which are not intended for general audience."
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:133
+#, c-format
+msgid "Total products"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, c-format
+msgid "Delete selected category from the database"
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, c-format
+msgid "Category delete successfully"
+msgstr ""
+
+#: src/paths/instance/categories/list/index.tsx:95
+#, c-format
+msgid "Could not delete the category"
+msgstr ""
+
+#: src/paths/instance/categories/create/CreatePage.tsx:77
+#, c-format
+msgid "Category name"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:53
+#, c-format
+msgid "Category added successfully"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:59
+#, c-format
+msgid "Could not add category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
+#, c-format
+msgid "Id:"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, c-format
+msgid "Name of the category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, c-format
+msgid "Search by product description or id"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
+#, c-format
+msgid "Products that this category will list."
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:93
+#, c-format
+msgid "Could not update category"
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:95
+#, c-format
+msgid "Category id is unknown"
+msgstr ""
+
+#: src/Routing.tsx:659
+#, c-format
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/Routing.tsx:703
#, c-format
-msgid "Check your token is valid"
+msgid "KYC verification needed"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/Routing.tsx:707
#, c-format
-msgid "Couldn't access the server."
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/components/menu/SideBar.tsx:157
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Configuration"
msgstr ""
-#: src/Application.tsx:104
+#: src/components/menu/SideBar.tsx:196
#, c-format
-msgid "Server not found"
+msgid "Settings"
msgstr ""
-#: src/Application.tsx:118
+#: src/components/menu/SideBar.tsx:206
#, c-format
-msgid "Server response with an error code"
+msgid "Access token"
msgstr ""
-#: src/Application.tsx:120
+#: src/components/menu/SideBar.tsx:214
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "Connection"
msgstr ""
-#: src/Application.tsx:131
+#: src/components/menu/SideBar.tsx:223
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid "Interface"
msgstr ""
-#: src/Application.tsx:144
+#: src/components/menu/SideBar.tsx:264
#, c-format
-msgid "Unexpected Error"
+msgid "List"
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/components/menu/SideBar.tsx:283
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Log out"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/paths/admin/create/index.tsx:54
#, c-format
-msgid "add element to the list"
+msgid "Failed to create instance"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/Application.tsx:208
#, c-format
-msgid "add"
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, c-format
+msgid "Contacting the server failed"
+msgstr ""
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2730,12 +3573,22 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot b/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
index 5ef56ca05..121687ca2 100644
--- a/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
+++ b/packages/merchant-backoffice-ui/src/i18n/taler-merchant-backoffice.pot
@@ -25,221 +25,812 @@ msgstr ""
"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
+#: src/components/ErrorLoadingMerchant.tsx:45
#, c-format
-msgid "Cancel"
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid "A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, c-format
+msgid "Unexpected request error."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, c-format
+msgid "Unexpected error."
msgstr ""
#: src/components/modal/index.tsx:79
#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/components/modal/index.tsx:87
+#, c-format
msgid "%1$s"
msgstr ""
-#: src/components/modal/index.tsx:84
+#: src/components/modal/index.tsx:92
#, c-format
msgid "Close"
msgstr ""
-#: src/components/modal/index.tsx:124
+#: src/components/modal/index.tsx:132
#, c-format
msgid "Continue"
msgstr ""
-#: src/components/modal/index.tsx:178
+#: src/components/modal/index.tsx:192
#, c-format
msgid "Clear"
msgstr ""
-#: src/components/modal/index.tsx:190
+#: src/components/modal/index.tsx:204
#, c-format
msgid "Confirm"
msgstr ""
-#: src/components/modal/index.tsx:296
+#: src/components/modal/index.tsx:246
+#, c-format
+msgid "Required"
+msgstr ""
+
+#: src/components/modal/index.tsx:248
+#, c-format
+msgid "Letter must be a JSON string"
+msgstr ""
+
+#: src/components/modal/index.tsx:250
+#, c-format
+msgid "JSON string is invalid"
+msgstr ""
+
+#: src/components/modal/index.tsx:255
+#, c-format
+msgid "Import"
+msgstr ""
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr ""
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account profile. "
+"Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, c-format
+msgid "Account information"
+msgstr ""
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr ""
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr ""
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr ""
+
+#: src/components/modal/index.tsx:400
+#, c-format
+msgid "Account id"
+msgstr ""
+
+#: src/components/modal/index.tsx:411
+#, c-format
+msgid "Owner's name"
+msgstr ""
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no longer be "
+"able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able to "
+"access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
#, c-format
-msgid "is not the same as the current access token"
+msgid "Purging an instance %1$s ."
msgstr ""
-#: src/components/modal/index.tsx:299
+#: src/components/modal/index.tsx:537
#, c-format
-msgid "cannot be empty"
+msgid "Is not the same as the current access token"
msgstr ""
-#: src/components/modal/index.tsx:301
+#: src/components/modal/index.tsx:542
#, c-format
-msgid "cannot be the same as the old token"
+msgid "Can't be the same as the old token"
msgstr ""
-#: src/components/modal/index.tsx:305
+#: src/components/modal/index.tsx:546
#, c-format
-msgid "is not the same"
+msgid "Is not the same"
msgstr ""
-#: src/components/modal/index.tsx:315
+#: src/components/modal/index.tsx:554
#, c-format
msgid "You are updating the access token from instance with id %1$s"
msgstr ""
-#: src/components/modal/index.tsx:331
+#: src/components/modal/index.tsx:570
#, c-format
msgid "Old access token"
msgstr ""
-#: src/components/modal/index.tsx:332
+#: src/components/modal/index.tsx:571
#, c-format
-msgid "access token currently in use"
+msgid "Access token currently in use"
msgstr ""
-#: src/components/modal/index.tsx:338
+#: src/components/modal/index.tsx:577
#, c-format
msgid "New access token"
msgstr ""
-#: src/components/modal/index.tsx:339
+#: src/components/modal/index.tsx:578
#, c-format
-msgid "next access token to be used"
+msgid "Next access token to be used"
msgstr ""
-#: src/components/modal/index.tsx:344
+#: src/components/modal/index.tsx:583
#, c-format
msgid "Repeat access token"
msgstr ""
-#: src/components/modal/index.tsx:345
+#: src/components/modal/index.tsx:584
#, c-format
-msgid "confirm the same access token"
+msgid "Confirm the same access token"
msgstr ""
-#: src/components/modal/index.tsx:350
+#: src/components/modal/index.tsx:589
#, c-format
msgid "Clearing the access token will mean public access to the instance"
msgstr ""
-#: src/components/modal/index.tsx:377
+#: src/components/modal/index.tsx:616
#, c-format
-msgid "cannot be the same as the old access token"
+msgid "Can't be the same as the old access token"
msgstr ""
-#: src/components/modal/index.tsx:394
+#: src/components/modal/index.tsx:631
#, c-format
msgid "You are setting the access token for the new instance"
msgstr ""
-#: src/components/modal/index.tsx:420
+#: src/components/modal/index.tsx:657
#, c-format
msgid "With external authorization method no check will be done by the merchant backend"
msgstr ""
-#: src/components/modal/index.tsx:436
+#: src/components/modal/index.tsx:673
#, c-format
msgid "Set external authorization"
msgstr ""
-#: src/components/modal/index.tsx:448
+#: src/components/modal/index.tsx:685
#, c-format
msgid "Set access token"
msgstr ""
-#: src/components/modal/index.tsx:470
+#: src/components/modal/index.tsx:707
#, c-format
msgid "Operation in progress..."
msgstr ""
-#: src/components/modal/index.tsx:479
+#: src/components/modal/index.tsx:716
#, c-format
msgid "The operation will be automatically canceled after %1$s seconds"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:80
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, c-format
+msgid "Your instance not found"
+msgstr ""
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr ""
+
+#: src/paths/login/index.tsx:95
+#, c-format
+msgid "Please enter your access token for %1$s."
+msgstr ""
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr ""
+
+#: src/paths/admin/list/TableActive.tsx:81
#, c-format
msgid "Instances"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:93
+#: src/paths/admin/list/TableActive.tsx:94
#, c-format
msgid "Delete"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:99
+#: src/paths/admin/list/TableActive.tsx:100
#, c-format
-msgid "add new instance"
+msgid "Add new instance"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:178
+#: src/paths/admin/list/TableActive.tsx:177
#, c-format
msgid "ID"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:181
+#: src/paths/admin/list/TableActive.tsx:180
#, c-format
msgid "Name"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:220
+#: src/paths/admin/list/TableActive.tsx:222
#, c-format
msgid "Edit"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:237
+#: src/paths/admin/list/TableActive.tsx:239
#, c-format
msgid "Purge"
msgstr ""
-#: src/paths/admin/list/TableActive.tsx:261
+#: src/paths/admin/list/TableActive.tsx:263
#, c-format
msgid "There is no instances yet, add more pressing the + sign"
msgstr ""
-#: src/paths/admin/list/View.tsx:68
+#: src/paths/admin/list/View.tsx:66
#, c-format
msgid "Only show active instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:71
+#: src/paths/admin/list/View.tsx:69
#, c-format
msgid "Active"
msgstr ""
-#: src/paths/admin/list/View.tsx:78
+#: src/paths/admin/list/View.tsx:76
#, c-format
msgid "Only show deleted instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:81
+#: src/paths/admin/list/View.tsx:79
#, c-format
msgid "Deleted"
msgstr ""
-#: src/paths/admin/list/View.tsx:88
+#: src/paths/admin/list/View.tsx:86
#, c-format
msgid "Show all instances"
msgstr ""
-#: src/paths/admin/list/View.tsx:91
+#: src/paths/admin/list/View.tsx:89
#, c-format
msgid "All"
msgstr ""
-#: src/paths/admin/list/index.tsx:101
+#: src/paths/admin/list/index.tsx:100
#, c-format
msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/admin/list/index.tsx:106
+#: src/paths/admin/list/index.tsx:105
#, c-format
msgid "Failed to delete instance"
msgstr ""
-#: src/paths/admin/list/index.tsx:124
+#: src/paths/admin/list/index.tsx:140
#, c-format
-msgid "Instance '%1$s' (ID: %2$s) has been disabled"
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
msgstr ""
-#: src/paths/admin/list/index.tsx:129
+#: src/paths/admin/list/index.tsx:145
#, c-format
msgid "Failed to purge instance"
msgstr ""
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, c-format
+msgid "This is not a valid host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, c-format
+msgid "International Bank Account Number."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, c-format
+msgid "Legal name of the person holding the account."
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, c-format
+msgid "Invalid url"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, c-format
+msgid "Server replied with \"bad request\"."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, c-format
+msgid "Account:"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL below "
+"to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire transfers "
+"to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, c-format
+msgid "Auth type"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, c-format
+msgid "Do not change"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, c-format
+msgid "Not verified"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, c-format
+msgid "Confirm operation"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, c-format
+msgid "Account details"
+msgstr ""
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, c-format
+msgid "Could not create account"
+msgstr ""
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr ""
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, c-format
+msgid "Bank accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, c-format
+msgid "Add new account"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, c-format
+msgid "Wire method: Bitcoin"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, c-format
+msgid "Delete selected accounts from the database"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, c-format
+msgid "Wire method: x-taler-bank"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, c-format
+msgid "Account name"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, c-format
+msgid "Wire method: IBAN"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, c-format
+msgid "Other accounts"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, c-format
+msgid "Bank account delete successfully"
+msgstr ""
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, c-format
+msgid "Could not delete the bank account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, c-format
+msgid "Could not update account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, c-format
+msgid "Could not delete account"
+msgstr ""
+
#: src/paths/instance/kyc/list/ListPage.tsx:41
#, c-format
msgid "Pending KYC verification"
@@ -262,57 +853,107 @@ msgstr ""
#: src/paths/instance/kyc/list/ListPage.tsx:109
#, c-format
-msgid "KYC URL"
+msgid "Reason"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:144
+#: src/paths/instance/kyc/list/ListPage.tsx:122
#, c-format
-msgid "Code"
+msgid "There is an anti-money laundering process pending to complete."
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:147
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:167
#, c-format
msgid "Http Status"
msgstr ""
-#: src/paths/instance/kyc/list/ListPage.tsx:177
+#: src/paths/instance/kyc/list/ListPage.tsx:197
#, c-format
msgid "No pending kyc verification!"
msgstr ""
-#: src/components/form/InputDate.tsx:123
+#: src/components/form/InputDate.tsx:127
#, c-format
-msgid "change value to unknown date"
+msgid "Change value to unknown date"
msgstr ""
-#: src/components/form/InputDate.tsx:124
+#: src/components/form/InputDate.tsx:128
#, c-format
-msgid "change value to empty"
+msgid "Change value to empty"
msgstr ""
-#: src/components/form/InputDate.tsx:131
+#: src/components/form/InputDate.tsx:140
#, c-format
-msgid "clear"
+msgid "Change value to never"
msgstr ""
-#: src/components/form/InputDate.tsx:136
+#: src/components/form/InputDate.tsx:145
#, c-format
-msgid "change value to never"
+msgid "Never"
msgstr ""
-#: src/components/form/InputDate.tsx:141
+#: src/components/picker/DurationPicker.tsx:55
#, c-format
-msgid "never"
+msgid "days"
msgstr ""
-#: src/components/form/InputLocation.tsx:29
+#: src/components/picker/DurationPicker.tsx:65
#, c-format
-msgid "Country"
+msgid "hours"
msgstr ""
-#: src/components/form/InputLocation.tsx:33
+#: src/components/picker/DurationPicker.tsx:76
#, c-format
-msgid "Address"
+msgid "minutes"
+msgstr ""
+
+#: src/components/picker/DurationPicker.tsx:87
+#, c-format
+msgid "seconds"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:62
+#, c-format
+msgid "Forever"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr ""
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
msgstr ""
#: src/components/form/InputLocation.tsx:39
@@ -355,66 +996,61 @@ msgstr ""
msgid "Country subdivision"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:66
-#, c-format
-msgid "Product id"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:69
+#: src/components/form/InputSearchOnList.tsx:80
#, c-format
msgid "Description"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:94
-#, c-format
-msgid "Product"
-msgstr ""
-
-#: src/components/form/InputSearchProduct.tsx:95
+#: src/components/form/InputSearchOnList.tsx:106
#, c-format
-msgid "search products by it's description or id"
+msgid "Enter description or id"
msgstr ""
-#: src/components/form/InputSearchProduct.tsx:151
+#: src/components/form/InputSearchOnList.tsx:164
#, c-format
-msgid "no products found with that description"
+msgid "no match found with that description or id"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:56
+#: src/components/product/InventoryProductForm.tsx:57
#, c-format
msgid "You must enter a valid product identifier."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:64
+#: src/components/product/InventoryProductForm.tsx:65
#, c-format
msgid "Quantity must be greater than 0!"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:76
+#: src/components/product/InventoryProductForm.tsx:77
#, c-format
msgid ""
"This quantity exceeds remaining stock. Currently, only %1$s units remain "
"unreserved in stock."
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:109
+#: src/components/product/InventoryProductForm.tsx:100
+#, c-format
+msgid "Search product"
+msgstr ""
+
+#: src/components/product/InventoryProductForm.tsx:112
#, c-format
msgid "Quantity"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:110
+#: src/components/product/InventoryProductForm.tsx:113
#, c-format
-msgid "how many products will be added"
+msgid "How many products will be added"
msgstr ""
-#: src/components/product/InventoryProductForm.tsx:117
+#: src/components/product/InventoryProductForm.tsx:120
#, c-format
msgid "Add from inventory"
msgstr ""
#: src/components/form/InputImage.tsx:105
#, c-format
-msgid "Image should be smaller than 1 MB"
+msgid "Image must be smaller than 1 MB"
msgstr ""
#: src/components/form/InputImage.tsx:110
@@ -427,53 +1063,73 @@ msgstr ""
msgid "Remove"
msgstr ""
-#: src/components/form/InputTaxes.tsx:113
+#: src/components/form/InputTaxes.tsx:47
+#, c-format
+msgid "Invalid"
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
#, c-format
msgid "No taxes configured for this product."
msgstr ""
-#: src/components/form/InputTaxes.tsx:119
+#: src/components/form/InputTaxes.tsx:109
#, c-format
msgid "Amount"
msgstr ""
-#: src/components/form/InputTaxes.tsx:120
+#: src/components/form/InputTaxes.tsx:110
#, 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
+#: src/components/form/InputTaxes.tsx:112
#, 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
+#: src/components/form/InputTaxes.tsx:121
#, c-format
msgid "Legal name of the tax, e.g. VAT or import duties."
msgstr ""
-#: src/components/form/InputTaxes.tsx:137
+#: src/components/form/InputTaxes.tsx:127
#, c-format
-msgid "add tax to the tax list"
+msgid "Add tax to the tax list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:72
+#: src/components/product/NonInventoryProductForm.tsx:71
#, c-format
-msgid "describe and add a product that is not in the inventory list"
+msgid "Describe and add a product that is not in the inventory list"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:75
+#: src/components/product/NonInventoryProductForm.tsx:74
#, c-format
msgid "Add custom product"
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:86
+#: src/components/product/NonInventoryProductForm.tsx:85
#, c-format
msgid "Complete information of the product"
msgstr ""
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, c-format
+msgid "Must be a number"
+msgstr ""
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, c-format
+msgid "Must be grater than 0"
+msgstr ""
+
#: src/components/product/NonInventoryProductForm.tsx:185
#, c-format
msgid "Image"
@@ -481,12 +1137,12 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:186
#, c-format
-msgid "photo of the product"
+msgid "Photo of the product."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:192
#, c-format
-msgid "full product description"
+msgid "Full product description."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:196
@@ -496,7 +1152,7 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:197
#, c-format
-msgid "name of the product unit"
+msgid "Name of the product unit."
msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:201
@@ -506,2202 +1162,2389 @@ msgstr ""
#: src/components/product/NonInventoryProductForm.tsx:202
#, c-format
-msgid "amount in the current currency"
+msgid "Amount in the current currency."
msgstr ""
-#: src/components/product/NonInventoryProductForm.tsx:211
+#: src/components/product/NonInventoryProductForm.tsx:208
#, c-format
-msgid "Taxes"
+msgid "How many products will be added."
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
+#: src/components/product/NonInventoryProductForm.tsx:211
#, c-format
-msgid "quantity"
+msgid "Taxes"
msgstr ""
-#: src/components/product/ProductList.tsx:47
+#: src/components/product/ProductList.tsx:46
#, c-format
-msgid "unit price"
+msgid "Unit price"
msgstr ""
-#: src/components/product/ProductList.tsx:50
+#: src/components/product/ProductList.tsx:49
#, c-format
-msgid "total price"
+msgid "Total price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:153
+#: src/paths/instance/orders/create/CreatePage.tsx:162
#, c-format
-msgid "required"
+msgid "Must be greater than 0"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:157
+#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
-msgid "not valid"
+msgid "Refund deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:159
+#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
-msgid "must be greater than 0"
+msgid "Wire transfer deadline can't be before refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:164
+#: src/paths/instance/orders/create/CreatePage.tsx:188
#, c-format
-msgid "not a valid json"
+msgid "Wire transfer deadline can't be before pay deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:170
+#: src/paths/instance/orders/create/CreatePage.tsx:196
#, c-format
-msgid "should be in the future"
+msgid "Must have a refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:173
+#: src/paths/instance/orders/create/CreatePage.tsx:201
#, c-format
-msgid "refund deadline cannot be before pay deadline"
+msgid "Auto refund can't be after refund deadline"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:179
+#: src/paths/instance/orders/create/CreatePage.tsx:208
#, c-format
-msgid "wire transfer deadline cannot be before refund deadline"
+msgid "Must be in the future"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:190
+#: src/paths/instance/orders/create/CreatePage.tsx:376
#, c-format
-msgid "wire transfer deadline cannot be before pay deadline"
+msgid "Simple"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:197
+#: src/paths/instance/orders/create/CreatePage.tsx:388
#, c-format
-msgid "should have a refund deadline"
+msgid "Advanced"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:202
+#: src/paths/instance/orders/create/CreatePage.tsx:400
#, c-format
-msgid "auto refund cannot be after refund deadline"
+msgid "Manage products in order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:360
+#: src/paths/instance/orders/create/CreatePage.tsx:404
#, c-format
-msgid "Manage products in order"
+msgid "%1$s products with a total price of %2$s."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:369
+#: src/paths/instance/orders/create/CreatePage.tsx:411
#, c-format
msgid "Manage list of products in the order."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:391
+#: src/paths/instance/orders/create/CreatePage.tsx:435
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
-msgid "total product price added up"
+msgid "Total product price added up"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:430
+#: src/paths/instance/orders/create/CreatePage.tsx:474
#, c-format
msgid "Amount to be paid by the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:436
+#: src/paths/instance/orders/create/CreatePage.tsx:480
#, c-format
msgid "Order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:437
+#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
-msgid "final order price"
+msgid "Final order price"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:444
+#: src/paths/instance/orders/create/CreatePage.tsx:488
#, c-format
msgid "Summary"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:445
+#: src/paths/instance/orders/create/CreatePage.tsx:489
#, c-format
msgid "Title of the order to be shown to the customer"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:450
+#: src/paths/instance/orders/create/CreatePage.tsx:495
#, c-format
msgid "Shipping and Fulfillment"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:455
+#: src/paths/instance/orders/create/CreatePage.tsx:500
#, c-format
msgid "Delivery date"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:456
+#: src/paths/instance/orders/create/CreatePage.tsx:501
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:461
+#: src/paths/instance/orders/create/CreatePage.tsx:506
#, c-format
msgid "Location"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:462
+#: src/paths/instance/orders/create/CreatePage.tsx:507
#, c-format
-msgid "address where the products will be delivered"
+msgid "Address where the products will be delivered"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:469
+#: src/paths/instance/orders/create/CreatePage.tsx:514
#, c-format
msgid "Fulfillment URL"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:470
+#: src/paths/instance/orders/create/CreatePage.tsx:515
#, c-format
msgid "URL to which the user will be redirected after successful payment."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:476
+#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
msgid "Taler payment options"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:477
+#: src/paths/instance/orders/create/CreatePage.tsx:524
#, c-format
msgid "Override default Taler payment settings for this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:481
+#: src/paths/instance/orders/create/CreatePage.tsx:529
#, c-format
-msgid "Payment deadline"
+msgid "Payment time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:482
+#: src/paths/instance/orders/create/CreatePage.tsx:535
#, 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"
+"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."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:492
+#: src/paths/instance/orders/create/CreatePage.tsx:552
#, c-format
-msgid "Deadline for the exchange to make the wire transfer."
+msgid "Default"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:496
+#: src/paths/instance/orders/create/CreatePage.tsx:561
#, c-format
-msgid "Auto-refund deadline"
+msgid "Refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:497
+#: src/paths/instance/orders/create/CreatePage.tsx:569
#, c-format
msgid ""
-"Time until which the wallet will automatically check for refunds without user "
-"interaction."
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:502
+#: src/paths/instance/orders/create/CreatePage.tsx:594
#, c-format
-msgid "Maximum deposit fee"
+msgid "Wire transfer time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:503
+#: src/paths/instance/orders/create/CreatePage.tsx:602
#, 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."
+"Time for the exchange to make the wire transfer. Time starts after the order is "
+"created."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:507
+#: src/paths/instance/orders/create/CreatePage.tsx:628
#, c-format
-msgid "Maximum wire fee"
+msgid "Auto-refund time"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:508
+#: src/paths/instance/orders/create/CreatePage.tsx:634
#, 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."
+"Time until which the wallet will automatically check for refunds without user "
+"interaction."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:512
+#: src/paths/instance/orders/create/CreatePage.tsx:642
#, c-format
-msgid "Wire fee amortization"
+msgid "Maximum fee"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:513
+#: src/paths/instance/orders/create/CreatePage.tsx:643
#, 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."
+"Maximum 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:517
+#: src/paths/instance/orders/create/CreatePage.tsx:649
#, c-format
msgid "Create token"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:518
+#: src/paths/instance/orders/create/CreatePage.tsx:650
#, c-format
msgid ""
-"Uncheck this option if the merchant backend generated an order ID with enough "
-"entropy to prevent adversarial claims."
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:522
+#: src/paths/instance/orders/create/CreatePage.tsx:656
#, c-format
msgid "Minimum age required"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:523
+#: src/paths/instance/orders/create/CreatePage.tsx:657
#, 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
+#: src/paths/instance/orders/create/CreatePage.tsx:660
#, c-format
msgid "Min age defined by the producs is %1$s"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:534
+#: src/paths/instance/orders/create/CreatePage.tsx:661
#, c-format
-msgid "Additional information"
+msgid "No product with age restriction in this order"
msgstr ""
-#: src/paths/instance/orders/create/CreatePage.tsx:535
+#: src/paths/instance/orders/create/CreatePage.tsx:671
#, 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"
+msgid "Additional information"
msgstr ""
-#: src/components/picker/DurationPicker.tsx:76
+#: src/paths/instance/orders/create/CreatePage.tsx:672
#, c-format
-msgid "minutes"
+msgid "Custom information to be included in the contract for this order."
msgstr ""
-#: src/components/picker/DurationPicker.tsx:87
+#: src/paths/instance/orders/create/CreatePage.tsx:681
#, c-format
-msgid "seconds"
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
msgstr ""
-#: src/components/form/InputDuration.tsx:53
+#: src/paths/instance/orders/create/CreatePage.tsx:707
#, c-format
-msgid "forever"
+msgid "Custom field name"
msgstr ""
-#: src/components/form/InputDuration.tsx:62
+#: src/paths/instance/orders/create/CreatePage.tsx:793
#, c-format
-msgid "%1$sM"
+msgid "Disabled"
msgstr ""
-#: src/components/form/InputDuration.tsx:64
+#: src/paths/instance/orders/create/CreatePage.tsx:796
#, c-format
-msgid "%1$sY"
+msgid "No deadline"
msgstr ""
-#: src/components/form/InputDuration.tsx:66
+#: src/paths/instance/orders/create/CreatePage.tsx:797
#, c-format
-msgid "%1$sd"
+msgid "Deadline at %1$s"
msgstr ""
-#: src/components/form/InputDuration.tsx:68
+#: src/paths/instance/orders/create/index.tsx:109
#, c-format
-msgid "%1$sh"
+msgid "Could not create order"
msgstr ""
-#: src/components/form/InputDuration.tsx:70
+#: src/paths/instance/orders/create/index.tsx:111
#, c-format
-msgid "%1$smin"
+msgid "No exchange would accept a payment because of KYC requirements."
msgstr ""
-#: src/components/form/InputDuration.tsx:72
+#: src/paths/instance/orders/create/index.tsx:129
#, c-format
-msgid "%1$ssec"
+msgid "No more stock for product with id \"%1$s\"."
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:75
+#: src/paths/instance/orders/list/Table.tsx:77
#, c-format
msgid "Orders"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:81
+#: src/paths/instance/orders/list/Table.tsx:83
#, c-format
-msgid "create order"
+msgid "Create order"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:147
+#: src/paths/instance/orders/list/Table.tsx:140
#, c-format
-msgid "load newer orders"
+msgid "Load first page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:154
+#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
msgid "Date"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:200
+#: src/paths/instance/orders/list/Table.tsx:193
#, c-format
msgid "Refund"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:209
+#: src/paths/instance/orders/list/Table.tsx:202
#, c-format
msgid "copy url"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:225
+#: src/paths/instance/orders/list/Table.tsx:214
#, c-format
-msgid "load older orders"
+msgid "Load more orders after the last one"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:242
+#: src/paths/instance/orders/list/Table.tsx:216
#, c-format
-msgid "No orders have been found matching your query!"
-msgstr ""
-
-#: src/paths/instance/orders/list/Table.tsx:288
-#, c-format
-msgid "duplicated"
+msgid "Load next page"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:299
+#: src/paths/instance/orders/list/Table.tsx:233
#, 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"
+msgid "No orders have been found matching your query!"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:349
+#: src/paths/instance/orders/list/Table.tsx:280
#, c-format
-msgid "amount"
+msgid "Duplicated"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:352
+#: src/paths/instance/orders/list/Table.tsx:293
#, c-format
-msgid "reason"
+msgid "This value exceed the refundable amount"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:389
+#: src/paths/instance/orders/list/Table.tsx:381
#, c-format
-msgid "amount to be refunded"
+msgid "Amount to be refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:391
+#: src/paths/instance/orders/list/Table.tsx:383
#, 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
+#: src/paths/instance/orders/list/Table.tsx:391
#, c-format
-msgid "requested by the customer"
+msgid "Requested by the customer"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:400
+#: src/paths/instance/orders/list/Table.tsx:392
#, c-format
-msgid "other"
+msgid "Other"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:403
+#: src/paths/instance/orders/list/Table.tsx:395
#, c-format
-msgid "why this order is being refunded"
+msgid "Why this order is being refunded"
msgstr ""
-#: src/paths/instance/orders/list/Table.tsx:409
+#: src/paths/instance/orders/list/Table.tsx:401
#, c-format
-msgid "more information to give context"
+msgid "More information to give context"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:62
+#: src/paths/instance/orders/details/DetailPage.tsx:72
#, c-format
msgid "Contract Terms"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:68
+#: src/paths/instance/orders/details/DetailPage.tsx:78
#, c-format
-msgid "human-readable description of the whole purchase"
+msgid "Human-readable description of the whole purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:74
+#: src/paths/instance/orders/details/DetailPage.tsx:84
#, c-format
-msgid "total price for the transaction"
+msgid "Total price for the transaction"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:81
+#: src/paths/instance/orders/details/DetailPage.tsx:91
#, c-format
msgid "URL for this purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:87
+#: src/paths/instance/orders/details/DetailPage.tsx:97
#, c-format
msgid "Max fee"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:88
+#: src/paths/instance/orders/details/DetailPage.tsx:98
#, c-format
-msgid "maximum total deposit fee accepted by the merchant for this contract"
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:93
+#: src/paths/instance/orders/details/DetailPage.tsx:103
#, c-format
-msgid "Max wire fee"
+msgid "Created at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:94
+#: src/paths/instance/orders/details/DetailPage.tsx:104
#, c-format
-msgid "maximum wire fee accepted by the merchant"
+msgid "Time when this contract was generated"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:100
+#: src/paths/instance/orders/details/DetailPage.tsx:109
#, c-format
-msgid ""
-"over how many customer transactions does the merchant expect to amortize wire "
-"fees on average"
+msgid "Refund deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:105
+#: src/paths/instance/orders/details/DetailPage.tsx:110
#, c-format
-msgid "Created at"
+msgid "After this deadline has passed no refunds will be accepted"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:106
+#: src/paths/instance/orders/details/DetailPage.tsx:115
#, c-format
-msgid "time when this contract was generated"
+msgid "Payment deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:112
+#: src/paths/instance/orders/details/DetailPage.tsx:116
#, c-format
-msgid "after this deadline has passed no refunds will be accepted"
+msgid "After this deadline, the merchant won't accept payments for the contract"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:118
+#: src/paths/instance/orders/details/DetailPage.tsx:121
#, c-format
-msgid "after this deadline, the merchant won't accept payments for the contract"
+msgid "Wire transfer deadline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:124
+#: src/paths/instance/orders/details/DetailPage.tsx:122
#, c-format
-msgid "transfer deadline for the exchange"
+msgid "Transfer deadline for the exchange"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:130
+#: src/paths/instance/orders/details/DetailPage.tsx:128
#, c-format
-msgid "time indicating when the order should be delivered"
+msgid "Time indicating when the order should be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:136
+#: src/paths/instance/orders/details/DetailPage.tsx:134
#, c-format
-msgid "where the order will be delivered"
+msgid "Where the order will be delivered"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:144
+#: src/paths/instance/orders/details/DetailPage.tsx:142
#, c-format
msgid "Auto-refund delay"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:145
+#: src/paths/instance/orders/details/DetailPage.tsx:143
#, c-format
-msgid "how long the wallet should try to get an automatic refund for the purchase"
+msgid "How long the wallet should try to get an automatic refund for the purchase"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:150
+#: src/paths/instance/orders/details/DetailPage.tsx:148
#, c-format
msgid "Extra info"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:151
+#: src/paths/instance/orders/details/DetailPage.tsx:149
#, c-format
-msgid "extra data that is only interpreted by the merchant frontend"
+msgid "Extra data that is only interpreted by the merchant frontend"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:219
+#: src/paths/instance/orders/details/DetailPage.tsx:222
#, c-format
msgid "Order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:221
+#: src/paths/instance/orders/details/DetailPage.tsx:224
#, c-format
-msgid "claimed"
+msgid "Claimed"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:247
+#: src/paths/instance/orders/details/DetailPage.tsx:251
#, c-format
-msgid "claimed at"
+msgid "Claimed at"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:265
+#: src/paths/instance/orders/details/DetailPage.tsx:273
#, c-format
msgid "Timeline"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:271
+#: src/paths/instance/orders/details/DetailPage.tsx:279
#, c-format
msgid "Payment details"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:291
+#: src/paths/instance/orders/details/DetailPage.tsx:299
#, c-format
msgid "Order status"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:301
+#: src/paths/instance/orders/details/DetailPage.tsx:309
#, c-format
msgid "Product list"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:451
+#: src/paths/instance/orders/details/DetailPage.tsx:461
#, c-format
-msgid "paid"
+msgid "Paid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:455
+#: src/paths/instance/orders/details/DetailPage.tsx:465
#, c-format
-msgid "wired"
+msgid "Wired"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:460
+#: src/paths/instance/orders/details/DetailPage.tsx:470
#, c-format
-msgid "refunded"
+msgid "Refunded"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:480
+#: src/paths/instance/orders/details/DetailPage.tsx:490
#, c-format
-msgid "refund order"
+msgid "Refund order"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:481
+#: src/paths/instance/orders/details/DetailPage.tsx:491
#, c-format
-msgid "not refundable"
+msgid "Not refundable"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:489
+#: src/paths/instance/orders/details/DetailPage.tsx:521
#, c-format
-msgid "refund"
+msgid "Next event in"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:553
+#: src/paths/instance/orders/details/DetailPage.tsx:557
#, c-format
msgid "Refunded amount"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:560
+#: src/paths/instance/orders/details/DetailPage.tsx:564
#, c-format
msgid "Refund taken"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:570
+#: src/paths/instance/orders/details/DetailPage.tsx:574
#, c-format
msgid "Status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:583
+#: src/paths/instance/orders/details/DetailPage.tsx:587
#, c-format
msgid "Refund URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:636
+#: src/paths/instance/orders/details/DetailPage.tsx:641
#, c-format
-msgid "unpaid"
+msgid "Unpaid"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:654
+#: src/paths/instance/orders/details/DetailPage.tsx:659
#, c-format
-msgid "pay at"
+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
+#: src/paths/instance/orders/details/DetailPage.tsx:712
#, c-format
msgid "Order status URL"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:711
+#: src/paths/instance/orders/details/DetailPage.tsx:716
#, c-format
msgid "Payment URI"
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:740
+#: src/paths/instance/orders/details/DetailPage.tsx:745
#, c-format
msgid "Unknown order status. This is an error, please contact the administrator."
msgstr ""
-#: src/paths/instance/orders/details/DetailPage.tsx:767
+#: src/paths/instance/orders/details/DetailPage.tsx:772
#, 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
+#: src/paths/instance/orders/details/index.tsx:88
#, c-format
-msgid "could not create the refund"
+msgid "Refund created successfully"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:78
+#: src/paths/instance/orders/details/index.tsx:95
#, c-format
-msgid "select date to show nearby orders"
+msgid "Could not create the refund"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:94
+#: src/paths/instance/orders/details/index.tsx:97
#, c-format
-msgid "order id"
+msgid "There are pending KYC requirements."
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:100
+#: src/components/form/JumpToElementById.tsx:39
#, c-format
-msgid "jump to order with the given order ID"
+msgid "Missing id"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:122
+#: src/components/form/JumpToElementById.tsx:48
#, c-format
-msgid "remove all filters"
+msgid "Not found"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:132
+#: src/paths/instance/orders/list/ListPage.tsx:83
#, c-format
-msgid "only show paid orders"
+msgid "Select date to show nearby orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:135
+#: src/paths/instance/orders/list/ListPage.tsx:96
#, c-format
-msgid "Paid"
+msgid "Only show paid orders"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:142
+#: src/paths/instance/orders/list/ListPage.tsx:99
#, c-format
-msgid "only show orders with refunds"
+msgid "New"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:145
+#: src/paths/instance/orders/list/ListPage.tsx:116
#, c-format
-msgid "Refunded"
+msgid "Only show orders with refunds"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:152
+#: src/paths/instance/orders/list/ListPage.tsx:126
#, c-format
msgid ""
-"only show orders where customers paid, but wire payments from payment provider "
+"Only show orders where customers paid, but wire payments from payment provider "
"are still pending"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:155
+#: src/paths/instance/orders/list/ListPage.tsx:129
#, c-format
msgid "Not wired"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:170
+#: src/paths/instance/orders/list/ListPage.tsx:139
#, c-format
-msgid "clear date filter"
+msgid "Completed"
msgstr ""
-#: src/paths/instance/orders/list/ListPage.tsx:184
+#: src/paths/instance/orders/list/ListPage.tsx:146
#, c-format
-msgid "date (YYYY/MM/DD)"
+msgid "Remove all filters"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:103
+#: src/paths/instance/orders/list/ListPage.tsx:164
#, c-format
-msgid "Enter an order id"
+msgid "Clear date filter"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:111
+#: src/paths/instance/orders/list/ListPage.tsx:178
#, c-format
-msgid "order not found"
+msgid "Jump to date (%1$s)"
msgstr ""
-#: src/paths/instance/orders/list/index.tsx:178
+#: src/paths/instance/orders/list/index.tsx:113
#, c-format
-msgid "could not get the order to refund"
+msgid "Jump to order with the given product ID"
msgstr ""
-#: src/components/exception/AsyncButton.tsx:43
+#: src/paths/instance/orders/list/index.tsx:114
#, c-format
-msgid "Loading..."
+msgid "Order id"
msgstr ""
-#: src/components/form/InputStock.tsx:99
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
#, c-format
-msgid ""
-"click here to configure the stock of the product, leave it as is and the backend "
-"will not control stock"
+msgid "Invalid. Only characters and numbers"
msgstr ""
-#: src/components/form/InputStock.tsx:109
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
#, c-format
-msgid "Manage stock"
+msgid "Just letters and numbers from 2 to 7"
msgstr ""
-#: src/components/form/InputStock.tsx:115
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
#, c-format
-msgid "this product has been configured without stock control"
+msgid "Size of the key must be 32"
msgstr ""
-#: src/components/form/InputStock.tsx:119
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
#, c-format
-msgid "Infinite"
+msgid "Internal id on the system"
msgstr ""
-#: src/components/form/InputStock.tsx:136
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
#, c-format
-msgid "lost cannot be greater than current and incoming (max %1$s)"
+msgid "Useful to identify the device physically"
msgstr ""
-#: src/components/form/InputStock.tsx:176
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
#, c-format
-msgid "Incoming"
+msgid "Verification algorithm"
msgstr ""
-#: src/components/form/InputStock.tsx:177
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
#, c-format
-msgid "Lost"
+msgid "Algorithm to use to verify transaction in offline mode"
msgstr ""
-#: src/components/form/InputStock.tsx:192
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
#, c-format
-msgid "Current"
+msgid "Device key"
msgstr ""
-#: src/components/form/InputStock.tsx:196
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
#, c-format
-msgid "remove stock control for this product"
+msgid "Be sure to be very hard to guess or use the random generator"
msgstr ""
-#: src/components/form/InputStock.tsx:202
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
#, c-format
-msgid "without stock"
+msgid "Your device need to have exactly the same value"
msgstr ""
-#: src/components/form/InputStock.tsx:211
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
#, c-format
-msgid "Next restock"
+msgid "Generate random secret key"
msgstr ""
-#: src/components/form/InputStock.tsx:217
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
#, c-format
-msgid "Delivery address"
+msgid "Random"
msgstr ""
-#: src/components/product/ProductForm.tsx:133
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
#, c-format
-msgid "product identification to use in URLs (for internal use only)"
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
msgstr ""
-#: src/components/product/ProductForm.tsx:139
+#: src/paths/instance/otp_devices/create/index.tsx:60
#, c-format
-msgid "illustration of the product for customers"
+msgid "Device added successfully"
msgstr ""
-#: src/components/product/ProductForm.tsx:145
+#: src/paths/instance/otp_devices/create/index.tsx:66
#, c-format
-msgid "product description for customers"
+msgid "Could not add device"
msgstr ""
-#: src/components/product/ProductForm.tsx:149
+#: src/paths/instance/otp_devices/list/Table.tsx:57
#, c-format
-msgid "Age restricted"
+msgid "OTP Devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:150
+#: src/paths/instance/otp_devices/list/Table.tsx:62
#, c-format
-msgid "is this product restricted for customer below certain age?"
+msgid "Add new devices"
msgstr ""
-#: src/components/product/ProductForm.tsx:155
+#: src/paths/instance/otp_devices/list/Table.tsx:117
#, c-format
-msgid ""
-"unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 "
-"meters) for customers"
+msgid "Load more devices before the first one"
msgstr ""
-#: src/components/product/ProductForm.tsx:160
+#: src/paths/instance/otp_devices/list/Table.tsx:155
#, c-format
-msgid "sale price for customers, including taxes, for above units of the product"
+msgid "Delete selected devices from the database"
msgstr ""
-#: src/components/product/ProductForm.tsx:164
+#: src/paths/instance/otp_devices/list/Table.tsx:170
#, c-format
-msgid "Stock"
+msgid "Load more devices after the last one"
msgstr ""
-#: src/components/product/ProductForm.tsx:166
+#: src/paths/instance/otp_devices/list/Table.tsx:190
#, c-format
-msgid "product inventory for products with finite supply (for internal use only)"
+msgid "There is no devices yet, add more pressing the + sign"
msgstr ""
-#: src/components/product/ProductForm.tsx:171
+#: src/paths/instance/otp_devices/list/index.tsx:90
#, c-format
-msgid "taxes included in the product price, exposed to customers"
+msgid "Device delete successfully"
msgstr ""
-#: src/paths/instance/products/create/CreatePage.tsx:66
+#: src/paths/instance/otp_devices/list/index.tsx:95
#, c-format
-msgid "Need to complete marked fields"
+msgid "Could not delete the device"
msgstr ""
-#: src/paths/instance/products/create/index.tsx:51
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
#, c-format
-msgid "could not create product"
+msgid "Device:"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:68
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
#, c-format
-msgid "Products"
+msgid "Not modified"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:73
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
#, c-format
-msgid "add product to inventory"
+msgid "Change key"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:137
+#: src/paths/instance/otp_devices/update/index.tsx:119
#, c-format
-msgid "Sell"
+msgid "Could not update template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:143
+#: src/paths/instance/otp_devices/update/index.tsx:121
#, c-format
-msgid "Profit"
+msgid "Template id is unknown"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:149
+#: src/paths/instance/otp_devices/update/index.tsx:129
#, c-format
-msgid "Sold"
+msgid "The provided information is inconsistent with the current state of the template"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:210
+#: src/components/form/InputStock.tsx:99
#, c-format
-msgid "free"
+msgid ""
+"Click here to configure the stock of the product, leave it as is and the backend "
+"will not control stock."
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:248
+#: src/components/form/InputStock.tsx:109
#, c-format
-msgid "go to product update page"
+msgid "Manage stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:255
+#: src/components/form/InputStock.tsx:115
#, c-format
-msgid "Update"
+msgid "This product has been configured without stock control"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:260
+#: src/components/form/InputStock.tsx:119
#, c-format
-msgid "remove this product from the database"
+msgid "Infinite"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:331
+#: src/components/form/InputStock.tsx:136
#, c-format
-msgid "update the product with new price"
+msgid "Lost can't be greater than current and incoming (max %1$s)"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:341
+#: src/components/form/InputStock.tsx:169
#, c-format
-msgid "update product with new price"
+msgid "Incoming"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:399
+#: src/components/form/InputStock.tsx:170
#, c-format
-msgid "add more elements to the inventory"
+msgid "Lost"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:404
+#: src/components/form/InputStock.tsx:185
#, c-format
-msgid "report elements lost in the inventory"
+msgid "Current"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:409
+#: src/components/form/InputStock.tsx:189
#, c-format
-msgid "new price for the product"
+msgid "Remove stock control for this product"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:421
+#: src/components/form/InputStock.tsx:195
#, c-format
-msgid "the are value with errors"
+msgid "without stock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:422
+#: src/components/form/InputStock.tsx:204
#, c-format
-msgid "update product with new stock and price"
+msgid "Next restock"
msgstr ""
-#: src/paths/instance/products/list/Table.tsx:463
+#: src/components/form/InputStock.tsx:208
#, c-format
-msgid "There is no products yet, add more pressing the + sign"
+msgid "Warehouse address"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:86
+#: src/components/form/InputArray.tsx:118
#, c-format
-msgid "product updated successfully"
+msgid "Add element to the list"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:92
+#: src/components/product/ProductForm.tsx:120
#, c-format
-msgid "could not update the product"
+msgid "Invalid amount"
msgstr ""
-#: src/paths/instance/products/list/index.tsx:103
+#: src/components/product/ProductForm.tsx:181
#, c-format
-msgid "product delete successfully"
+msgid "Product identification to use in URLs (for internal use only)."
msgstr ""
-#: src/paths/instance/products/list/index.tsx:109
+#: src/components/product/ProductForm.tsx:187
#, c-format
-msgid "could not delete the product"
+msgid "Illustration of the product for customers."
msgstr ""
-#: src/paths/instance/products/update/UpdatePage.tsx:56
+#: src/components/product/ProductForm.tsx:193
#, c-format
-msgid "Product id:"
+msgid "Product description for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:95
+#: src/components/product/ProductForm.tsx:197
#, 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."
+msgid "Age restriction"
msgstr ""
-#: src/paths/instance/reserves/create/CreatedSuccessfully.tsx:102
+#: src/components/product/ProductForm.tsx:198
#, c-format
-msgid "If your system supports RFC 8905, you can do this by opening this URI:"
+msgid "Is this product restricted for customer below certain age?"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:83
+#: src/components/product/ProductForm.tsx:199
#, c-format
-msgid "it should be greater than 0"
+msgid "Minimum age of the customer"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:88
+#: src/components/product/ProductForm.tsx:203
#, c-format
-msgid "must be a valid URL"
+msgid "Unit name"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:107
+#: src/components/product/ProductForm.tsx:204
#, c-format
-msgid "Initial balance"
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 "
+"meters) for customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:108
+#: src/components/product/ProductForm.tsx:205
#, c-format
-msgid "balance prior to deposit"
+msgid "Example: kg, items or liters"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:112
+#: src/components/product/ProductForm.tsx:209
#, c-format
-msgid "Exchange URL"
+msgid "Price per unit"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:113
+#: src/components/product/ProductForm.tsx:210
#, c-format
-msgid "URL of exchange"
+msgid "Sale price for customers, including taxes, for above units of the product."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:148
+#: src/components/product/ProductForm.tsx:214
#, c-format
-msgid "Next"
+msgid "Stock"
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:186
+#: src/components/product/ProductForm.tsx:216
#, c-format
-msgid "Wire method"
+msgid "Inventory for products with finite supply (for internal use only)."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:187
+#: src/components/product/ProductForm.tsx:221
#, c-format
-msgid "method to use for wire transfer"
+msgid "Taxes included in the product price, exposed to customers."
msgstr ""
-#: src/paths/instance/reserves/create/CreatePage.tsx:189
+#: src/components/product/ProductForm.tsx:225
#, c-format
-msgid "Select one wire method"
+msgid "Categories"
msgstr ""
-#: src/paths/instance/reserves/create/index.tsx:62
+#: src/components/product/ProductForm.tsx:231
#, c-format
-msgid "could not create reserve"
+msgid "Search by category description or id"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:77
+#: src/components/product/ProductForm.tsx:232
#, c-format
-msgid "Valid until"
+msgid "Categories where this product will be listed on."
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:82
+#: src/paths/instance/products/create/index.tsx:52
#, c-format
-msgid "Created balance"
+msgid "Product created successfully"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:99
+#: src/paths/instance/products/create/index.tsx:58
#, c-format
-msgid "Exchange balance"
+msgid "Could not create product"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:104
+#: src/paths/instance/products/list/Table.tsx:73
#, c-format
-msgid "Picked up"
+msgid "Inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:109
+#: src/paths/instance/products/list/Table.tsx:78
#, c-format
-msgid "Committed"
+msgid "Add product to inventory"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:116
+#: src/paths/instance/products/list/Table.tsx:160
#, c-format
-msgid "Account address"
+msgid "Sales"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:119
+#: src/paths/instance/products/list/Table.tsx:166
#, c-format
-msgid "Subject"
+msgid "Sold"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:130
+#: src/paths/instance/products/list/Table.tsx:230
#, c-format
-msgid "Tips"
+msgid "Free"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:193
+#: src/paths/instance/products/list/Table.tsx:271
#, c-format
-msgid "No tips has been authorized from this reserve"
+msgid "Go to product update page"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:213
+#: src/paths/instance/products/list/Table.tsx:278
#, c-format
-msgid "Authorized"
+msgid "Update"
msgstr ""
-#: src/paths/instance/reserves/details/DetailPage.tsx:222
+#: src/paths/instance/products/list/Table.tsx:283
#, c-format
-msgid "Expiration"
+msgid "Remove this product from the database"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:108
+#: src/paths/instance/products/list/Table.tsx:318
#, c-format
-msgid "amount of tip"
+msgid "Load more products after the last one"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:112
+#: src/paths/instance/products/list/Table.tsx:361
#, c-format
-msgid "Justification"
+msgid "Update the product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:114
+#: src/paths/instance/products/list/Table.tsx:373
#, c-format
-msgid "reason for the tip"
+msgid "Update product with new price"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:118
+#: src/paths/instance/products/list/Table.tsx:384
#, c-format
-msgid "URL after tip"
+msgid "Confirm update"
msgstr ""
-#: src/paths/instance/reserves/list/AutorizeTipModal.tsx:119
+#: src/paths/instance/products/list/Table.tsx:431
#, c-format
-msgid "URL to visit after tip payment"
+msgid "Add more elements to the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:65
+#: src/paths/instance/products/list/Table.tsx:436
#, c-format
-msgid "Reserves not yet funded"
+msgid "Report elements lost in the inventory"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:89
+#: src/paths/instance/products/list/Table.tsx:441
#, c-format
-msgid "Reserves ready"
+msgid "New price for the product"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:95
+#: src/paths/instance/products/list/Table.tsx:453
#, c-format
-msgid "add new reserve"
+msgid "The are value with errors"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:143
+#: src/paths/instance/products/list/Table.tsx:454
#, c-format
-msgid "Expires at"
+msgid "Update product with new stock and price"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:146
+#: src/paths/instance/products/list/Table.tsx:495
#, c-format
-msgid "Initial"
+msgid "There is no products yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:202
+#: src/paths/instance/products/list/index.tsx:86
#, c-format
-msgid "delete selected reserve from the database"
+msgid "Jump to product with the given product ID"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:210
+#: src/paths/instance/products/list/index.tsx:87
#, c-format
-msgid "authorize new tip from selected reserve"
+msgid "Product id"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:237
+#: src/paths/instance/products/list/index.tsx:104
#, c-format
-msgid "There is no ready reserves yet, add more pressing the + sign or fund them"
+msgid "Product updated successfully"
msgstr ""
-#: src/paths/instance/reserves/list/Table.tsx:264
+#: src/paths/instance/products/list/index.tsx:109
#, c-format
-msgid "Expected Balance"
+msgid "Could not update the product"
msgstr ""
-#: src/paths/instance/reserves/list/index.tsx:110
+#: src/paths/instance/products/list/index.tsx:144
#, c-format
-msgid "could not create the tip"
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:77
+#: src/paths/instance/products/list/index.tsx:149
#, c-format
-msgid "should not be empty"
+msgid "Could not delete the product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:93
+#: src/paths/instance/products/list/index.tsx:165
#, c-format
-msgid "should be greater that 0"
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:96
+#: src/paths/instance/products/list/index.tsx:173
+#, c-format
+msgid "Deleting an product can't be undone."
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
#, c-format
-msgid "can't be empty"
+msgid "Product id:"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:100
+#: src/paths/instance/products/update/index.tsx:85
#, c-format
-msgid "to short"
+msgid "Product (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:108
+#: src/paths/instance/products/update/index.tsx:91
#, c-format
-msgid "just letters and numbers from 2 to 7"
+msgid "Could not update product"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:110
+#: src/paths/instance/templates/create/CreatePage.tsx:96
#, c-format
-msgid "size of the key should be 32"
+msgid "Invalid. only characters and numbers"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:137
+#: src/paths/instance/templates/create/CreatePage.tsx:112
+#, c-format
+msgid "Must be greater that 0"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:119
+#, c-format
+msgid "To short"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:192
#, c-format
msgid "Identifier"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:138
+#: src/paths/instance/templates/create/CreatePage.tsx:193
#, c-format
msgid "Name of the template in URLs."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:144
+#: src/paths/instance/templates/create/CreatePage.tsx:199
#, c-format
msgid "Describe what this template stands for"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:149
+#: src/paths/instance/templates/create/CreatePage.tsx:206
#, c-format
-msgid "Fixed summary"
+msgid "If specified, this template will create order with the same summary"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:150
+#: src/paths/instance/templates/create/CreatePage.tsx:210
#, c-format
-msgid "If specified, this template will create order with the same summary"
+msgid "Summary is editable"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:154
+#: src/paths/instance/templates/create/CreatePage.tsx:211
#, c-format
-msgid "Fixed price"
+msgid "Allow the user to change the summary."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:155
+#: src/paths/instance/templates/create/CreatePage.tsx:217
#, c-format
msgid "If specified, this template will create order with the same price"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:159
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, c-format
+msgid "Amount is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
#, c-format
msgid "Minimum age"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:161
+#: src/paths/instance/templates/create/CreatePage.tsx:243
#, c-format
msgid "Is this contract restricted to some age?"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:165
+#: src/paths/instance/templates/create/CreatePage.tsx:247
#, c-format
msgid "Payment timeout"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:167
+#: src/paths/instance/templates/create/CreatePage.tsx:249
#, 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
+#: src/paths/instance/templates/create/CreatePage.tsx:254
#, c-format
-msgid "Verification algorithm"
+msgid "OTP device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:172
+#: src/paths/instance/templates/create/CreatePage.tsx:255
#, c-format
-msgid "Algorithm to use to verify transaction in offline mode"
+msgid "Use to verify transaction while offline."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:180
+#: src/paths/instance/templates/create/CreatePage.tsx:257
#, c-format
-msgid "Point-of-sale key"
+msgid "No OTP device."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:182
+#: src/paths/instance/templates/create/CreatePage.tsx:259
#, c-format
-msgid "Useful to validate the purchase"
+msgid "Add one first"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:196
+#: src/paths/instance/templates/create/CreatePage.tsx:272
#, c-format
-msgid "generate random secret key"
+msgid "No device"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:203
+#: src/paths/instance/templates/create/CreatePage.tsx:276
#, c-format
-msgid "random"
+msgid "Use to verify transaction in offline mode."
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:208
+#: src/paths/instance/templates/create/index.tsx:52
#, c-format
-msgid "show secret key"
+msgid "Template has been created"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:209
+#: src/paths/instance/templates/create/index.tsx:58
#, c-format
-msgid "hide secret key"
+msgid "Could not create template"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:216
+#: src/paths/instance/templates/list/Table.tsx:61
#, c-format
-msgid "hide"
+msgid "Templates"
msgstr ""
-#: src/paths/instance/templates/create/CreatePage.tsx:218
+#: src/paths/instance/templates/list/Table.tsx:66
#, c-format
-msgid "show"
+msgid "Add new templates"
msgstr ""
-#: src/paths/instance/templates/create/index.tsx:52
+#: src/paths/instance/templates/list/Table.tsx:127
#, c-format
-msgid "could not inform template"
+msgid "Load more templates before the first one"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:54
+#: src/paths/instance/templates/list/Table.tsx:165
#, c-format
-msgid "Amount is required"
+msgid "Delete selected templates from the database"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:58
+#: src/paths/instance/templates/list/Table.tsx:172
#, c-format
-msgid "Order summary is required"
+msgid "Use template to create new order"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:86
+#: src/paths/instance/templates/list/Table.tsx:175
#, c-format
-msgid "New order for template"
+msgid "Use template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:108
+#: src/paths/instance/templates/list/Table.tsx:179
#, c-format
-msgid "Amount of the order"
+msgid "Create qr code for the template"
msgstr ""
-#: src/paths/instance/templates/use/UsePage.tsx:113
+#: src/paths/instance/templates/list/Table.tsx:194
#, c-format
-msgid "Order summary"
+msgid "Load more templates after the last one"
msgstr ""
-#: src/paths/instance/templates/use/index.tsx:92
+#: src/paths/instance/templates/list/Table.tsx:214
#, c-format
-msgid "could not create order from template"
+msgid "There is no templates yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:131
+#: src/paths/instance/templates/list/index.tsx:91
#, 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."
+msgid "Jump to template with the given template ID"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:92
+#, c-format
+msgid "Template identification"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:132
+#, c-format
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:137
+#, c-format
+msgid "Failed to delete template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:148
+#: src/paths/instance/templates/list/index.tsx:153
#, c-format
-msgid "Fixed amount"
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:149
+#: src/paths/instance/templates/list/index.tsx:160
#, c-format
-msgid "Default amount"
+msgid "Deleting an template"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:161
+#: src/paths/instance/templates/list/index.tsx:162
#, c-format
-msgid "Default summary"
+msgid "can't be undone"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:177
+#: src/paths/instance/templates/qr/QrPage.tsx:77
#, c-format
msgid "Print"
msgstr ""
-#: src/paths/instance/templates/qr/QrPage.tsx:184
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
#, c-format
-msgid "Setup TOTP"
+msgid "Too short"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:65
+#: src/paths/instance/templates/update/index.tsx:90
#, c-format
-msgid "Templates"
+msgid "Template (ID: %1$s) has been updated"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:70
+#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
-msgid "add new templates"
+msgid "Amount is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:142
+#: src/paths/instance/templates/use/UsePage.tsx:59
#, c-format
-msgid "load more templates before the first one"
+msgid "Order summary is required"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:146
+#: src/paths/instance/templates/use/UsePage.tsx:86
#, c-format
-msgid "load newer templates"
+msgid "New order for template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:181
+#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
-msgid "delete selected templates from the database"
+msgid "Amount of the order"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:188
+#: src/paths/instance/templates/use/UsePage.tsx:113
#, c-format
-msgid "use template to create new order"
+msgid "Order summary"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:195
+#: src/paths/instance/templates/use/index.tsx:125
#, c-format
-msgid "create qr code for the template"
+msgid "Could not create order from template"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:210
+#: src/paths/instance/token/DetailPage.tsx:57
#, c-format
-msgid "load more templates after the last one"
+msgid "You need your access token to perform the operation"
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:214
+#: src/paths/instance/token/DetailPage.tsx:74
#, c-format
-msgid "load older templates"
+msgid "You are updating the access token from instance with id \"%1$s\""
msgstr ""
-#: src/paths/instance/templates/list/Table.tsx:231
+#: src/paths/instance/token/DetailPage.tsx:105
#, c-format
-msgid "There is no templates yet, add more pressing the + sign"
+msgid "This instance doesn't have authentication token."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:104
+#: src/paths/instance/token/DetailPage.tsx:106
#, c-format
-msgid "template delete successfully"
+msgid "You can leave it empty if there is another layer of security."
msgstr ""
-#: src/paths/instance/templates/list/index.tsx:110
+#: src/paths/instance/token/DetailPage.tsx:121
#, c-format
-msgid "could not delete the template"
+msgid "Current access token"
msgstr ""
-#: src/paths/instance/templates/update/index.tsx:90
+#: src/paths/instance/token/DetailPage.tsx:126
#, c-format
-msgid "could not update template"
+msgid "Clearing the access token will mean public access to the instance."
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:57
+#: src/paths/instance/token/DetailPage.tsx:142
#, c-format
-msgid "should be one of '%1$s'"
+msgid "Clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#: src/paths/instance/token/DetailPage.tsx:177
#, c-format
-msgid "Webhook ID to use"
+msgid "Confirm change"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#: src/paths/instance/token/index.tsx:83
#, c-format
-msgid "Event"
+msgid "Failed to clear token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:90
+#: src/paths/instance/token/index.tsx:109
#, c-format
-msgid "The event of the webhook: why the webhook is used"
+msgid "Failed to set new token"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:94
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
#, c-format
-msgid "Method"
+msgid "Slug"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
#, c-format
-msgid "Method used by the webhook"
+msgid "Token family slug to use in URLs (for internal use only)"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
#, c-format
-msgid "URL"
+msgid "Kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:100
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
#, c-format
-msgid "URL of the webhook where the customer will be redirected"
+msgid "Token family kind"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
#, c-format
-msgid "Header"
+msgid "User-readable token family name"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:106
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
#, c-format
-msgid "Header template of the webhook"
+msgid "Token family description for customers"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:111
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
#, c-format
-msgid "Body"
+msgid "Valid After"
msgstr ""
-#: src/paths/instance/webhooks/create/CreatePage.tsx:112
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
#, c-format
-msgid "Body template by the webhook"
+msgid "Token family can issue tokens after this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:61
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
#, c-format
-msgid "Webhooks"
+msgid "Valid Before"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:66
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
#, c-format
-msgid "add new webhooks"
+msgid "Token family can issue tokens until this date"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:137
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
#, c-format
-msgid "load more webhooks before the first one"
+msgid "Duration"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:141
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
#, c-format
-msgid "load newer webhooks"
+msgid "Validity duration of a issued token"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:151
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
#, c-format
-msgid "Event type"
+msgid "Token familty created successfully"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:176
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
#, c-format
-msgid "delete selected webhook from the database"
+msgid "Could not create token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:198
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
#, c-format
-msgid "load more webhooks after the last one"
+msgid "Token Families"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:202
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
#, c-format
-msgid "load older webhooks"
+msgid "Add token family"
msgstr ""
-#: src/paths/instance/webhooks/list/Table.tsx:219
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
#, c-format
-msgid "There is no webhooks yet, add more pressing the + sign"
+msgid "Go to token family update page"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:94
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
#, c-format
-msgid "webhook delete successfully"
+msgid "Remove this token family from the database"
msgstr ""
-#: src/paths/instance/webhooks/list/index.tsx:100
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
#, c-format
-msgid "could not delete the webhook"
+msgid "There are no token families yet, add the first one by pressing the + sign."
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:63
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
#, c-format
-msgid "check the id, does not look valid"
+msgid "Token family updated successfully"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:65
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
#, c-format
-msgid "should have 52 characters, current %1$s"
+msgid "Could not update the token family"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:72
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, c-format
+msgid "Failed to delete token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
+#, c-format
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will become "
+"invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, c-format
+msgid "Token familty updated successfully"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
+#, c-format
+msgid "Could not update token family"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
+#, c-format
+msgid "Check the id, does not look valid"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
+#, c-format
+msgid "Must have 52 characters, current %1$s"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
#, c-format
msgid "URL doesn't have the right format"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
#, c-format
msgid "Credited bank account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:100
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
#, c-format
msgid "Select one account"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:101
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Bank account of the merchant where the payment was received"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:105
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
#, c-format
msgid "Wire transfer ID"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:107
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
#, c-format
msgid ""
-"unique identifier of the wire transfer used by the exchange, must be 52 "
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:112
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
#, 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
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
#, c-format
msgid "Amount credited"
msgstr ""
-#: src/paths/instance/transfers/create/CreatePage.tsx:118
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
msgstr ""
-#: src/paths/instance/transfers/create/index.tsx:58
+#: src/paths/instance/transfers/create/index.tsx:62
#, c-format
-msgid "could not inform transfer"
+msgid "Wire transfer informed successfully"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:61
+#: src/paths/instance/transfers/create/index.tsx:68
#, c-format
-msgid "Transfers"
+msgid "Could not inform transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:66
+#: src/paths/instance/transfers/list/Table.tsx:59
#, c-format
-msgid "add new transfer"
+msgid "Transfers"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:129
+#: src/paths/instance/transfers/list/Table.tsx:64
#, c-format
-msgid "load more transfers before the first one"
+msgid "Add new transfer"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:133
+#: src/paths/instance/transfers/list/Table.tsx:117
#, c-format
-msgid "load newer transfers"
+msgid "Load more transfers before the first one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:143
+#: src/paths/instance/transfers/list/Table.tsx:130
#, c-format
msgid "Credit"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:152
+#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
msgid "Confirmed"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:155
+#: src/paths/instance/transfers/list/Table.tsx:136
#, c-format
msgid "Verified"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:158
+#: src/paths/instance/transfers/list/Table.tsx:139
#, c-format
msgid "Executed at"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "yes"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:171
+#: src/paths/instance/transfers/list/Table.tsx:150
#, c-format
msgid "no"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:181
+#: src/paths/instance/transfers/list/Table.tsx:155
#, c-format
-msgid "unknown"
+msgid "never"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:187
+#: src/paths/instance/transfers/list/Table.tsx:160
#, c-format
-msgid "delete selected transfer from the database"
+msgid "unknown"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:202
+#: src/paths/instance/transfers/list/Table.tsx:166
#, c-format
-msgid "load more transfer after the last one"
+msgid "Delete selected transfer from the database"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:206
+#: src/paths/instance/transfers/list/Table.tsx:181
#, c-format
-msgid "load older transfers"
+msgid "Load more transfers after the last one"
msgstr ""
-#: src/paths/instance/transfers/list/Table.tsx:223
+#: src/paths/instance/transfers/list/Table.tsx:201
#, c-format
msgid "There is no transfer yet, add more pressing the + sign"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:79
+#: src/paths/instance/transfers/list/ListPage.tsx:76
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:83
+#, c-format
+msgid "All accounts"
+msgstr ""
+
+#: src/paths/instance/transfers/list/ListPage.tsx:84
#, c-format
-msgid "filter by account address"
+msgid "Filter by account address"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:100
+#: src/paths/instance/transfers/list/ListPage.tsx:105
#, c-format
-msgid "only show wire transfers confirmed by the merchant"
+msgid "Only show wire transfers confirmed by the merchant"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:110
+#: src/paths/instance/transfers/list/ListPage.tsx:115
#, c-format
-msgid "only show wire transfers claimed by the exchange"
+msgid "Only show wire transfers claimed by the exchange"
msgstr ""
-#: src/paths/instance/transfers/list/ListPage.tsx:113
+#: src/paths/instance/transfers/list/ListPage.tsx:118
#, c-format
msgid "Unverified"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:69
+#: src/paths/instance/transfers/list/index.tsx:118
#, c-format
-msgid "is not valid"
+msgid "Wire transfer \"%1$s...\" has been deleted"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:94
+#: src/paths/instance/transfers/list/index.tsx:123
#, c-format
-msgid "is not a number"
+msgid "Failed to delete transfer"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:96
+#: src/paths/admin/create/CreatePage.tsx:86
#, c-format
-msgid "must be 1 or greater"
+msgid "Must be business or individual"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:107
+#: src/paths/admin/create/CreatePage.tsx:104
#, c-format
-msgid "max 7 lines"
+msgid "Pay delay can't be greater than wire transfer delay"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:178
+#: src/paths/admin/create/CreatePage.tsx:112
#, c-format
-msgid "change authorization configuration"
+msgid "Max 7 lines"
msgstr ""
-#: src/paths/admin/create/CreatePage.tsx:217
+#: src/paths/admin/create/CreatePage.tsx:138
#, c-format
-msgid "Need to complete marked fields and choose authorization method"
+msgid "Doesn't match"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:82
+#: src/paths/admin/create/CreatePage.tsx:215
#, c-format
-msgid "This is not a valid bitcoin address."
+msgid "Enable access control"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:95
+#: src/paths/admin/create/CreatePage.tsx:216
#, c-format
-msgid "This is not a valid Ethereum address."
+msgid "Choose if the backend server should authenticate access."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:118
+#: src/paths/admin/create/CreatePage.tsx:243
#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
+msgid "Access control is not yet decided. This instance can't be created."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:120
+#: src/paths/admin/create/CreatePage.tsx:250
#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
+msgid "Authorization must be handled externally."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:128
+#: src/paths/admin/create/CreatePage.tsx:256
#, c-format
-msgid "IBAN country code not found"
+msgid "Authorization is handled by the backend server."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:153
+#: src/paths/admin/create/CreatePage.tsx:274
#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
+msgid "Need to complete marked fields and choose authorization method"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:248
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
#, c-format
-msgid "Target type"
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it is "
+"used to administer other instances."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:249
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
-msgid "Method to use for wire transfer"
+msgid "Business name"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:258
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
#, c-format
-msgid "Routing"
+msgid "Legal name of the business represented by this instance."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:259
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
#, c-format
-msgid "Routing number."
+msgid "Email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:263
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
#, c-format
-msgid "Account"
+msgid "Contact email"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:264
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
#, c-format
-msgid "Account number."
+msgid "Website URL"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:273
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
#, c-format
-msgid "Business Identifier Code."
+msgid "URL."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:282
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
#, c-format
-msgid "Bank Account Number."
+msgid "Logo"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:292
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
#, c-format
-msgid "Unified Payment Interface."
+msgid "Logo image."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:301
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
#, c-format
-msgid "Bitcoin protocol."
+msgid "Physical location of the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:310
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
#, c-format
-msgid "Ethereum protocol."
+msgid "Jurisdiction"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:319
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
#, c-format
-msgid "Interledger protocol."
+msgid "Jurisdiction for legal disputes with the merchant."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:328
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
#, c-format
-msgid "Host"
+msgid "Pay transaction fee"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:329
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
#, c-format
-msgid "Bank host."
+msgid "Assume the cost of the transaction of let the user pay for it."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:334
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
-msgid "Bank account."
+msgid "Default payment delay"
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:343
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
#, c-format
-msgid "Bank account owner's name."
+msgid "Time customers have to pay an order before the offer expires by default."
msgstr ""
-#: src/components/form/InputPaytoForm.tsx:370
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
-msgid "No accounts yet."
+msgid "Default wire transfer delay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:52
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
#, c-format
msgid ""
-"Name of the instance in URLs. The 'default' instance is special in that it is "
-"used to administer other instances."
+"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/components/instance/DefaultInstanceFormFields.tsx:58
+#: src/paths/instance/update/UpdatePage.tsx:124
#, c-format
-msgid "Business name"
+msgid "Instance id"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#: src/paths/instance/update/index.tsx:108
#, c-format
-msgid "Legal name of the business represented by this instance."
+msgid "Failed to update instance"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:64
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
#, c-format
-msgid "Email"
+msgid "Must be \"pay\" or \"refund\""
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:65
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
#, c-format
-msgid "Contact email"
+msgid "Must be one of '%1$s'"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:70
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
#, c-format
-msgid "Website URL"
+msgid "Webhook ID to use"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:71
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
#, c-format
-msgid "URL."
+msgid "Event"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:76
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
#, c-format
-msgid "Logo"
+msgid "Pay"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:77
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
-msgid "Logo image."
+msgid "The event of the webhook: why the webhook is used"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:82
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
-msgid "Bank account"
+msgid "Method"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:83
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
#, c-format
-msgid "URI specifying bank account for crediting revenue."
+msgid "GET"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:88
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
#, c-format
-msgid "Default max deposit fee"
+msgid "POST"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:89
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
#, c-format
-msgid "Maximum deposit fees this merchant is willing to pay per order by default."
+msgid "PUT"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "PATCH"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
#, c-format
-msgid "Default max wire fee"
+msgid "HEAD"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:95
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
#, c-format
-msgid "Maximum wire fees this merchant is willing to pay per wire transfer by default."
+msgid "Method used by the webhook"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:100
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
#, c-format
-msgid "Default wire fee amortization"
+msgid "URL"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
#, c-format
msgid ""
-"Number of orders excess wire transfer fees will be divided by to compute per "
-"order surcharge."
+"The text below support %1$s template engine. Any string between %2$s and %3$s "
+"will be replaced with replaced with the value of the corresponding variable."
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
#, c-format
-msgid "Physical location of the merchant."
+msgid "For example %1$s will be replaced with the the order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
#, c-format
-msgid "Jurisdiction"
+msgid "The short list of variables are:"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
#, c-format
-msgid "Jurisdiction for legal disputes with the merchant."
+msgid "order's description"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:122
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
#, c-format
-msgid "Default payment delay"
+msgid "order's price"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:124
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
#, c-format
-msgid "Time customers have to pay an order before the offer expires by default."
+msgid "order's unique identification"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:129
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
#, c-format
-msgid "Default wire transfer delay"
+msgid "the amount that was being refunded"
msgstr ""
-#: src/components/instance/DefaultInstanceFormFields.tsx:130
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
#, 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."
+msgid "the reason entered by the merchant staff for granting the refund"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:164
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
#, c-format
-msgid "Instance id"
+msgid "time of the refund in nanoseconds since 1970"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:173
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
#, c-format
-msgid "Change the authorization method use for this instance."
+msgid "Http body"
msgstr ""
-#: src/paths/instance/update/UpdatePage.tsx:182
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
#, c-format
-msgid "Manage access token"
+msgid "Body template by the webhook"
msgstr ""
-#: src/paths/instance/update/index.tsx:112
+#: src/paths/instance/webhooks/create/index.tsx:52
#, c-format
-msgid "Failed to create instance"
+msgid "Webhook create successfully"
msgstr ""
-#: src/components/exception/login.tsx:74
+#: src/paths/instance/webhooks/create/index.tsx:58
#, c-format
-msgid "Login required"
+msgid "Could not create the webhook"
msgstr ""
-#: src/components/exception/login.tsx:80
+#: src/paths/instance/webhooks/create/index.tsx:66
#, c-format
-msgid "Please enter your access token."
+msgid "Could not create webhook"
msgstr ""
-#: src/components/exception/login.tsx:108
+#: src/paths/instance/webhooks/list/Table.tsx:57
#, c-format
-msgid "Access Token"
+msgid "Webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:171
+#: src/paths/instance/webhooks/list/Table.tsx:62
#, c-format
-msgid "The request to the backend take too long and was cancelled"
+msgid "Add new webhooks"
msgstr ""
-#: src/InstanceRoutes.tsx:172
+#: src/paths/instance/webhooks/list/Table.tsx:117
#, c-format
-msgid "Diagnostic from %1$s is \"%2$s\""
+msgid "Load more webhooks before the first one"
msgstr ""
-#: src/InstanceRoutes.tsx:178
+#: src/paths/instance/webhooks/list/Table.tsx:130
#, c-format
-msgid "The backend reported a problem: HTTP status #%1$s"
+msgid "Event type"
msgstr ""
-#: src/InstanceRoutes.tsx:179
+#: src/paths/instance/webhooks/list/Table.tsx:155
#, c-format
-msgid "Diagnostic from %1$s is '%2$s'"
+msgid "Delete selected webhook from the database"
msgstr ""
-#: src/InstanceRoutes.tsx:196
+#: src/paths/instance/webhooks/list/Table.tsx:170
#, c-format
-msgid "Access denied"
+msgid "Load more webhooks after the last one"
msgstr ""
-#: src/InstanceRoutes.tsx:197
+#: src/paths/instance/webhooks/list/Table.tsx:190
#, c-format
-msgid "The access token provided is invalid."
+msgid "There is no webhooks yet, add more pressing the + sign"
msgstr ""
-#: src/InstanceRoutes.tsx:212
+#: src/paths/instance/webhooks/list/index.tsx:88
#, c-format
-msgid "No 'default' instance configured yet."
+msgid "Webhook delete successfully"
msgstr ""
-#: src/InstanceRoutes.tsx:213
+#: src/paths/instance/webhooks/list/index.tsx:93
#, c-format
-msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgid "Could not delete the webhook"
msgstr ""
-#: src/InstanceRoutes.tsx:630
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
#, c-format
-msgid "The access token provided is invalid"
+msgid "Header"
msgstr ""
-#: src/InstanceRoutes.tsx:664
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
#, c-format
-msgid "Hide for today"
+msgid "Header template of the webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:82
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
#, c-format
-msgid "Instance"
+msgid "Body"
msgstr ""
-#: src/components/menu/SideBar.tsx:91
+#: src/paths/instance/webhooks/update/index.tsx:88
#, c-format
-msgid "Settings"
+msgid "Webhook updated"
msgstr ""
-#: src/components/menu/SideBar.tsx:167
+#: src/paths/instance/webhooks/update/index.tsx:94
#, c-format
-msgid "Connection"
+msgid "Could not update webhook"
msgstr ""
-#: src/components/menu/SideBar.tsx:209
+#: src/paths/settings/index.tsx:73
#, c-format
-msgid "New"
+msgid "Language"
msgstr ""
-#: src/components/menu/SideBar.tsx:219
+#: src/paths/settings/index.tsx:96
#, c-format
-msgid "List"
+msgid "Set default"
msgstr ""
-#: src/components/menu/SideBar.tsx:234
+#: src/paths/settings/index.tsx:102
#, c-format
-msgid "Log out"
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, c-format
+msgid "Date format"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:71
+#: src/paths/settings/index.tsx:131
#, c-format
-msgid "Check your token is valid"
+msgid "How the date is going to be displayed"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:90
+#: src/paths/settings/index.tsx:134
#, c-format
-msgid "Couldn't access the server."
+msgid "Developer mode"
msgstr ""
-#: src/ApplicationReadyRoutes.tsx:91
+#: src/paths/settings/index.tsx:135
#, c-format
-msgid "Could not infer instance id from url %1$s"
+msgid "Shows more options and tools which are not intended for general audience."
msgstr ""
-#: src/Application.tsx:104
+#: src/paths/instance/categories/list/Table.tsx:133
#, c-format
-msgid "Server not found"
+msgid "Total products"
msgstr ""
-#: src/Application.tsx:118
+#: src/paths/instance/categories/list/Table.tsx:164
#, c-format
-msgid "Server response with an error code"
+msgid "Delete selected category from the database"
msgstr ""
-#: src/Application.tsx:120
+#: src/paths/instance/categories/list/Table.tsx:199
#, c-format
-msgid "Got message %1$s from %2$s"
+msgid "There is no categories yet, add more pressing the + sign"
msgstr ""
-#: src/Application.tsx:131
+#: src/paths/instance/categories/list/index.tsx:90
#, c-format
-msgid "Response from server is unreadable, http status: %1$s"
+msgid "Category delete successfully"
msgstr ""
-#: src/Application.tsx:144
+#: src/paths/instance/categories/list/index.tsx:95
#, c-format
-msgid "Unexpected Error"
+msgid "Could not delete the category"
msgstr ""
-#: src/components/form/InputArray.tsx:101
+#: src/paths/instance/categories/create/CreatePage.tsx:77
#, c-format
-msgid "The value %1$s is invalid for a payment url"
+msgid "Category name"
msgstr ""
-#: src/components/form/InputArray.tsx:110
+#: src/paths/instance/categories/create/index.tsx:53
#, c-format
-msgid "add element to the list"
+msgid "Category added successfully"
msgstr ""
-#: src/components/form/InputArray.tsx:112
+#: src/paths/instance/categories/create/index.tsx:59
#, c-format
-msgid "add"
+msgid "Could not add category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
+#, c-format
+msgid "Id:"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, c-format
+msgid "Name of the category"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, c-format
+msgid "Search by product description or id"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
+#, c-format
+msgid "Products that this category will list."
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:93
+#, c-format
+msgid "Could not update category"
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:95
+#, c-format
+msgid "Category id is unknown"
+msgstr ""
+
+#: src/Routing.tsx:659
+#, c-format
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
+msgstr ""
+
+#: src/Routing.tsx:703
+#, c-format
+msgid "KYC verification needed"
+msgstr ""
+
+#: src/Routing.tsx:707
+#, c-format
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:157
+#, c-format
+msgid "Configuration"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:196
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:206
+#, c-format
+msgid "Access token"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:214
+#, c-format
+msgid "Connection"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:223
+#, c-format
+msgid "Interface"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:264
+#, c-format
+msgid "List"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:283
+#, c-format
+msgid "Log out"
+msgstr ""
+
+#: src/paths/admin/create/index.tsx:54
+#, c-format
+msgid "Failed to create instance"
+msgstr ""
+
+#: src/Application.tsx:208
+#, c-format
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, c-format
+msgid "Contacting the server failed"
+msgstr ""
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
msgstr ""
#: src/components/form/InputSecured.tsx:37
@@ -2714,12 +3557,22 @@ msgstr ""
msgid "Changing"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:87
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr ""
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, c-format
+msgid "Business Name"
+msgstr ""
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
#, c-format
msgid "Order ID"
msgstr ""
-#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:101
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
#, c-format
msgid "Payment URL"
msgstr ""
diff --git a/packages/merchant-backoffice-ui/src/i18n/tr.po b/packages/merchant-backoffice-ui/src/i18n/tr.po
new file mode 100644
index 000000000..379b2426c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/i18n/tr.po
@@ -0,0 +1,2724 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-09-14 05:26+0000\n"
+"Last-Translator: Muha Aliss <muhaaliss@tuta.io>\n"
+"Language-Team: Turkish <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/tr/>\n"
+"Language: tr\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.5.5\n"
+
+#: src/components/modal/index.tsx:71
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: 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 ""
+
+#: 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 "Ülke"
+
+#: src/components/form/InputLocation.tsx:33
+#, c-format
+msgid "Address"
+msgstr ""
+
+#: src/components/form/InputLocation.tsx:39
+#, c-format
+msgid "Building number"
+msgstr "Bina numarası"
+
+#: src/components/form/InputLocation.tsx:41
+#, c-format
+msgid "Building name"
+msgstr "Bina adı"
+
+#: src/components/form/InputLocation.tsx:42
+#, c-format
+msgid "Street"
+msgstr "Sokak"
+
+#: src/components/form/InputLocation.tsx:43
+#, c-format
+msgid "Post code"
+msgstr "Posta kodu"
+
+#: src/components/form/InputLocation.tsx:44
+#, c-format
+msgid "Town location"
+msgstr "Kasaba konumu"
+
+#: src/components/form/InputLocation.tsx:45
+#, c-format
+msgid "Town"
+msgstr "Kasaba"
+
+#: src/components/form/InputLocation.tsx:46
+#, c-format
+msgid "District"
+msgstr "Semt"
+
+#: 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 "Miktar"
+
+#: 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 "Fiyat"
+
+#: 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 "Özet"
+
+#: 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 "Teslim tarihi"
+
+#: 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 "Gönderim URL'si"
+
+#: 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 "Geri ödeme son tarihi"
+
+#: 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 "Banka havalesi amortismanı"
+
+#: 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 "Tarih"
+
+#: 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 "Azami ücret"
+
+#: 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 "Oluşturulma"
+
+#: 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 "İade edildi"
+
+#: 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 "Ürünler"
+
+#: 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 "ücretsiz"
+
+#: 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 "Geçerlilik"
+
+#: 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 "Konu"
+
+#: 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 "Asgari yaş"
+
+#: 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 "Kredi"
+
+#: 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 "Hesap"
+
+#: 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 "Banka hesabı"
+
+#: 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/merchant-backoffice-ui/src/i18n/uk.po b/packages/merchant-backoffice-ui/src/i18n/uk.po
new file mode 100644
index 000000000..382e9909e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/i18n/uk.po
@@ -0,0 +1,4124 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-08-01 11:40+0000\n"
+"Last-Translator: Vlada Svirsh <vlada.svirsh@students.bfh.ch>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.5.5\n"
+
+#: src/components/ErrorLoadingMerchant.tsx:45
+#, c-format
+msgid "The request reached a timeout, check your connection."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:65
+#, c-format
+msgid "The request was cancelled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:107
+#, c-format
+msgid ""
+"A lot of request were made to the same server and this action was throttled."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:130
+#, c-format
+msgid "The response of the request is malformed."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:150
+#, c-format
+msgid "Could not complete the request due to a network problem."
+msgstr ""
+
+#: src/components/ErrorLoadingMerchant.tsx:171
+#, fuzzy, c-format
+msgid "Unexpected request error."
+msgstr "Несподівана помилка"
+
+#: src/components/ErrorLoadingMerchant.tsx:199
+#, fuzzy, c-format
+msgid "Unexpected error."
+msgstr "Несподівана помилка"
+
+#: src/components/modal/index.tsx:79
+#, c-format
+msgid "Cancel"
+msgstr "Скасувати"
+
+#: src/components/modal/index.tsx:87
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/modal/index.tsx:92
+#, c-format
+msgid "Close"
+msgstr "Закрити"
+
+#: src/components/modal/index.tsx:132
+#, c-format
+msgid "Continue"
+msgstr "Продовжити"
+
+#: src/components/modal/index.tsx:192
+#, c-format
+msgid "Clear"
+msgstr "Очистити"
+
+#: src/components/modal/index.tsx:204
+#, c-format
+msgid "Confirm"
+msgstr "Підтвердити"
+
+#: src/components/modal/index.tsx:246
+#, fuzzy, c-format
+msgid "Required"
+msgstr "обовʼязково"
+
+#: src/components/modal/index.tsx:248
+#, c-format
+msgid "Letter must be a JSON string"
+msgstr ""
+
+#: src/components/modal/index.tsx:250
+#, c-format
+msgid "JSON string is invalid"
+msgstr ""
+
+#: src/components/modal/index.tsx:255
+#, c-format
+msgid "Import"
+msgstr ""
+
+#: src/components/modal/index.tsx:256
+#, c-format
+msgid "Importing an account from the bank"
+msgstr ""
+
+#: src/components/modal/index.tsx:263
+#, c-format
+msgid ""
+"You can export your account settings from the Libeufin Bank's account "
+"profile. Paste the content in the next field."
+msgstr ""
+
+#: src/components/modal/index.tsx:271
+#, fuzzy, c-format
+msgid "Account information"
+msgstr "Додаткова інформація"
+
+#: src/components/modal/index.tsx:336
+#, c-format
+msgid "Correct form"
+msgstr ""
+
+#: src/components/modal/index.tsx:337
+#, c-format
+msgid "Comparing account details"
+msgstr ""
+
+#: src/components/modal/index.tsx:343
+#, c-format
+msgid ""
+"Testing against the account info URL succeeded but the account information "
+"reported is different with the account details form."
+msgstr ""
+
+#: src/components/modal/index.tsx:353
+#, c-format
+msgid "Field"
+msgstr ""
+
+#: src/components/modal/index.tsx:356
+#, c-format
+msgid "In the form"
+msgstr ""
+
+#: src/components/modal/index.tsx:359
+#, c-format
+msgid "Reported"
+msgstr ""
+
+#: src/components/modal/index.tsx:366
+#, c-format
+msgid "Type"
+msgstr ""
+
+#: src/components/modal/index.tsx:374
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/components/modal/index.tsx:383
+#, c-format
+msgid "Address"
+msgstr "Адреса"
+
+#: src/components/modal/index.tsx:393
+#, c-format
+msgid "Host"
+msgstr "Хост"
+
+#: src/components/modal/index.tsx:400
+#, fuzzy, c-format
+msgid "Account id"
+msgstr "Рахунок"
+
+#: src/components/modal/index.tsx:411
+#, fuzzy, c-format
+msgid "Owner's name"
+msgstr "Назва бізнесу"
+
+#: src/components/modal/index.tsx:445
+#, c-format
+msgid ""
+"If you delete the instance named %1$s (ID: %2$s), the merchant will no "
+"longer be able to process orders or refunds"
+msgstr ""
+
+#: src/components/modal/index.tsx:452
+#, c-format
+msgid ""
+"This action deletes the instance private key, but preserves all transaction "
+"data. You can still access that data after deleting the instance."
+msgstr ""
+
+#: src/components/modal/index.tsx:459
+#, c-format
+msgid "Deleting an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:487
+#, c-format
+msgid ""
+"If you purge the instance named %1$s (ID: %2$s), you will also delete all "
+"it&apos;s transaction data."
+msgstr ""
+
+#: src/components/modal/index.tsx:494
+#, c-format
+msgid ""
+"The instance will disappear from your list, and you will no longer be able "
+"to access it&apos;s data."
+msgstr ""
+
+#: src/components/modal/index.tsx:500
+#, c-format
+msgid "Purging an instance %1$s ."
+msgstr ""
+
+#: src/components/modal/index.tsx:537
+#, fuzzy, c-format
+msgid "Is not the same as the current access token"
+msgstr "не співпадає з поточним токеном доступу"
+
+#: src/components/modal/index.tsx:542
+#, fuzzy, c-format
+msgid "Can't be the same as the old token"
+msgstr "не може бути таким самим, як старий токен"
+
+#: src/components/modal/index.tsx:546
+#, fuzzy, c-format
+msgid "Is not the same"
+msgstr "не співпадає"
+
+#: src/components/modal/index.tsx:554
+#, c-format
+msgid "You are updating the access token from instance with id %1$s"
+msgstr "Ви оновлюєте токен доступу з інстанції з ідентифікатором %1$s"
+
+#: src/components/modal/index.tsx:570
+#, c-format
+msgid "Old access token"
+msgstr "Старий токен доступу"
+
+#: src/components/modal/index.tsx:571
+#, fuzzy, c-format
+msgid "Access token currently in use"
+msgstr "токен доступу, який зараз використовується"
+
+#: src/components/modal/index.tsx:577
+#, c-format
+msgid "New access token"
+msgstr "Новий токен доступу"
+
+#: src/components/modal/index.tsx:578
+#, fuzzy, c-format
+msgid "Next access token to be used"
+msgstr "наступний токен доступу, який буде використано"
+
+#: src/components/modal/index.tsx:583
+#, c-format
+msgid "Repeat access token"
+msgstr "Повторіть токен доступу"
+
+#: src/components/modal/index.tsx:584
+#, fuzzy, c-format
+msgid "Confirm the same access token"
+msgstr "підтвердити той самий токен доступу"
+
+#: src/components/modal/index.tsx:589
+#, c-format
+msgid "Clearing the access token will mean public access to the instance"
+msgstr "Видалення токена доступу означатиме публічний доступ до системи"
+
+#: src/components/modal/index.tsx:616
+#, fuzzy, c-format
+msgid "Can't be the same as the old access token"
+msgstr "не може бути таким самим, як старий токен доступу"
+
+#: src/components/modal/index.tsx:631
+#, c-format
+msgid "You are setting the access token for the new instance"
+msgstr "Ви встановлюєте токен доступу для нової інстанції"
+
+#: src/components/modal/index.tsx:657
+#, c-format
+msgid ""
+"With external authorization method no check will be done by the merchant "
+"backend"
+msgstr ""
+"З зовнішнім методом авторизації перевірка не буде виконуватися бекендом "
+"продавця"
+
+#: src/components/modal/index.tsx:673
+#, c-format
+msgid "Set external authorization"
+msgstr "Встановити зовнішню авторизацію"
+
+#: src/components/modal/index.tsx:685
+#, c-format
+msgid "Set access token"
+msgstr "Встановити токен доступу"
+
+#: src/components/modal/index.tsx:707
+#, c-format
+msgid "Operation in progress..."
+msgstr "Операція виконується..."
+
+#: src/components/modal/index.tsx:716
+#, c-format
+msgid "The operation will be automatically canceled after %1$s seconds"
+msgstr "Операція буде автоматично скасована через %1$s секунд"
+
+#: src/paths/login/index.tsx:63
+#, c-format
+msgid "Your password is incorrect"
+msgstr ""
+
+#: src/paths/login/index.tsx:70
+#, fuzzy, c-format
+msgid "Your instance not found"
+msgstr "замовлення не знайдено"
+
+#: src/paths/login/index.tsx:89
+#, c-format
+msgid "Login required"
+msgstr "Потрібен вхід"
+
+#: src/paths/login/index.tsx:95
+#, fuzzy, c-format
+msgid "Please enter your access token for %1$s."
+msgstr "Будь ласка, введіть ваш токен доступу."
+
+#: src/paths/login/index.tsx:102
+#, c-format
+msgid "Access Token"
+msgstr "Токен доступу"
+
+#: src/paths/admin/list/TableActive.tsx:81
+#, c-format
+msgid "Instances"
+msgstr "Інстанції"
+
+#: src/paths/admin/list/TableActive.tsx:94
+#, c-format
+msgid "Delete"
+msgstr "Видалити"
+
+#: src/paths/admin/list/TableActive.tsx:100
+#, fuzzy, c-format
+msgid "Add new instance"
+msgstr "додати нову інстанцію"
+
+#: src/paths/admin/list/TableActive.tsx:177
+#, c-format
+msgid "ID"
+msgstr "Ідентифікатор"
+
+#: src/paths/admin/list/TableActive.tsx:180
+#, c-format
+msgid "Name"
+msgstr "Назва"
+
+#: src/paths/admin/list/TableActive.tsx:222
+#, c-format
+msgid "Edit"
+msgstr "Редагувати"
+
+#: src/paths/admin/list/TableActive.tsx:239
+#, c-format
+msgid "Purge"
+msgstr "Очистити"
+
+#: src/paths/admin/list/TableActive.tsx:263
+#, c-format
+msgid "There is no instances yet, add more pressing the + sign"
+msgstr "Ще немає інстанцій, додайте більше, натиснувши знак +"
+
+#: src/paths/admin/list/View.tsx:66
+#, c-format
+msgid "Only show active instances"
+msgstr "Показувати тільки активні інстанції"
+
+#: src/paths/admin/list/View.tsx:69
+#, c-format
+msgid "Active"
+msgstr "Активні"
+
+#: src/paths/admin/list/View.tsx:76
+#, c-format
+msgid "Only show deleted instances"
+msgstr "Показувати тільки видалені інстанції"
+
+#: src/paths/admin/list/View.tsx:79
+#, c-format
+msgid "Deleted"
+msgstr "Видалено"
+
+#: src/paths/admin/list/View.tsx:86
+#, c-format
+msgid "Show all instances"
+msgstr "Показати всі інстанції"
+
+#: src/paths/admin/list/View.tsx:89
+#, c-format
+msgid "All"
+msgstr "Всі"
+
+#: src/paths/admin/list/index.tsx:100
+#, c-format
+msgid "Instance \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/admin/list/index.tsx:105
+#, c-format
+msgid "Failed to delete instance"
+msgstr "Не вдалося видалити інстанцію"
+
+#: src/paths/admin/list/index.tsx:140
+#, fuzzy, c-format
+msgid "Instance '%1$s' (ID: %2$s) has been purge"
+msgstr "Інстанція '%1$s' (ID: %2$s) була деактивована"
+
+#: src/paths/admin/list/index.tsx:145
+#, c-format
+msgid "Failed to purge instance"
+msgstr "Не вдалося очистити інстанцію"
+
+#: src/components/exception/AsyncButton.tsx:43
+#, c-format
+msgid "Loading..."
+msgstr "Завантаження..."
+
+#: src/components/form/InputPaytoForm.tsx:86
+#, c-format
+msgid "This is not a valid bitcoin address."
+msgstr "Це недійсна адреса біткойн."
+
+#: src/components/form/InputPaytoForm.tsx:99
+#, c-format
+msgid "This is not a valid Ethereum address."
+msgstr "Це недійсна адреса Ethereum."
+
+#: src/components/form/InputPaytoForm.tsx:122
+#, fuzzy, c-format
+msgid "This is not a valid host."
+msgstr "Це недійсна адреса біткойн."
+
+#: src/components/form/InputPaytoForm.tsx:145
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Номера IBAN зазвичай мають більше 4-ьох цифр"
+
+#: src/components/form/InputPaytoForm.tsx:147
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Номера IBAN зазвичай мають менше 34-ьох цифр"
+
+#: src/components/form/InputPaytoForm.tsx:155
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Код країни IBAN не знайдено"
+
+#: src/components/form/InputPaytoForm.tsx:180
+#, fuzzy, c-format
+msgid "IBAN number is invalid, checksum is wrong"
+msgstr "Номер IBAN не коректний, контрольна сума не сходиться"
+
+#: src/components/form/InputPaytoForm.tsx:195
+#, c-format
+msgid "Choose one..."
+msgstr "Виберіть одну..."
+
+#: src/components/form/InputPaytoForm.tsx:298
+#, c-format
+msgid "Method to use for wire transfer"
+msgstr "Метод для використання при банківському переказі"
+
+#: src/components/form/InputPaytoForm.tsx:308
+#, c-format
+msgid "Routing"
+msgstr "Маршрутизація"
+
+#: src/components/form/InputPaytoForm.tsx:310
+#, c-format
+msgid "Routing number."
+msgstr "Номер маршрутизації."
+
+#: src/components/form/InputPaytoForm.tsx:314
+#, c-format
+msgid "Account"
+msgstr "Рахунок"
+
+#: src/components/form/InputPaytoForm.tsx:316
+#, c-format
+msgid "Account number."
+msgstr "Номер рахунку."
+
+#: src/components/form/InputPaytoForm.tsx:324
+#, c-format
+msgid "Code"
+msgstr "Код"
+
+#: src/components/form/InputPaytoForm.tsx:326
+#, c-format
+msgid "Business Identifier Code."
+msgstr "Код ідентифікації бізнесу."
+
+#: src/components/form/InputPaytoForm.tsx:335
+#, fuzzy, c-format
+msgid "International Bank Account Number."
+msgstr "Номер банківського рахунку."
+
+#: src/components/form/InputPaytoForm.tsx:348
+#, c-format
+msgid "Unified Payment Interface."
+msgstr "Уніфікований інтерфейс платежів."
+
+#: src/components/form/InputPaytoForm.tsx:358
+#, c-format
+msgid "Bitcoin protocol."
+msgstr "Протокол біткойн."
+
+#: src/components/form/InputPaytoForm.tsx:368
+#, c-format
+msgid "Ethereum protocol."
+msgstr "Протокол Ethereum."
+
+#: src/components/form/InputPaytoForm.tsx:378
+#, c-format
+msgid "Interledger protocol."
+msgstr "Протокол Interledger."
+
+#: src/components/form/InputPaytoForm.tsx:400
+#, c-format
+msgid "Bank host."
+msgstr "Хост банку."
+
+#: src/components/form/InputPaytoForm.tsx:404
+#, c-format
+msgid "Without scheme and may include subpath:"
+msgstr ""
+
+#: src/components/form/InputPaytoForm.tsx:417
+#, c-format
+msgid "Bank account."
+msgstr "Банківський рахунок."
+
+#: src/components/form/InputPaytoForm.tsx:432
+#, fuzzy, c-format
+msgid "Legal name of the person holding the account."
+msgstr "назва одиниці продукту"
+
+#: src/components/form/InputPaytoForm.tsx:433
+#, c-format
+msgid "It should match the bank account name."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:104
+#, fuzzy, c-format
+msgid "Invalid url"
+msgstr "недійсний формат"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:106
+#, c-format
+msgid "URL must end with a '/'"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:108
+#, c-format
+msgid "URL must not contain params"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:110
+#, c-format
+msgid "URL must not hash param"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:186
+#, c-format
+msgid "The request to check the revenue API failed."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:195
+#, fuzzy, c-format
+msgid "Server replied with \"bad request\"."
+msgstr "Відповідь сервера з кодом помилки"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:203
+#, c-format
+msgid "Unauthorized, check credentials."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:211
+#, c-format
+msgid "The endpoint doesn't seems to be a Taler Revenue API."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:248
+#, fuzzy, c-format
+msgid "Account:"
+msgstr "Рахунок"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:272
+#, c-format
+msgid ""
+"If the bank supports Taler Revenue API then you can add the endpoint URL "
+"below to keep the revenue information in sync."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:281
+#, c-format
+msgid "Endpoint URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:284
+#, c-format
+msgid ""
+"From where the merchant can download information about incoming wire "
+"transfers to this account"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:288
+#, fuzzy, c-format
+msgid "Auth type"
+msgstr "Тип події"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:289
+#, c-format
+msgid "Choose the authentication type for the account info URL"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:292
+#, c-format
+msgid "Without authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:293
+#, c-format
+msgid "With authentication"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:294
+#, fuzzy, c-format
+msgid "Do not change"
+msgstr "URL обмінника"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:301
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:302
+#, c-format
+msgid "Username to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:307
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:308
+#, c-format
+msgid "Password to access the account information."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:313
+#, c-format
+msgid "Match"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:314
+#, c-format
+msgid "Check where the information match against the server info."
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:322
+#, fuzzy, c-format
+msgid "Not verified"
+msgstr "Неперевірений"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:324
+#, c-format
+msgid "Last test was ok"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:325
+#, c-format
+msgid "Last test failed"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:330
+#, c-format
+msgid "Compare info from server with account form"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:336
+#, c-format
+msgid "Test"
+msgstr ""
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:352
+#, c-format
+msgid "Need to complete marked fields"
+msgstr "Необхідно заповнити позначені поля"
+
+#: src/paths/instance/accounts/update/UpdatePage.tsx:353
+#, fuzzy, c-format
+msgid "Confirm operation"
+msgstr "Підтверджено"
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:212
+#, fuzzy, c-format
+msgid "Account details"
+msgstr "Адреса рахунку"
+
+#: src/paths/instance/accounts/create/CreatePage.tsx:291
+#, c-format
+msgid "Import from bank"
+msgstr ""
+
+#: src/paths/instance/accounts/create/index.tsx:67
+#, fuzzy, c-format
+msgid "Could not create account"
+msgstr "не вдалося створити продукт"
+
+#: src/paths/notfound/index.tsx:53
+#, c-format
+msgid "No 'default' instance configured yet."
+msgstr "Інстанція 'default' ще не налаштована."
+
+#: src/paths/notfound/index.tsx:54
+#, c-format
+msgid "Create a 'default' instance to begin using the merchant backoffice."
+msgstr ""
+"Створіть інстанцію 'default', щоб почати використовувати бекофіс продавця."
+
+#: src/paths/instance/accounts/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Bank accounts"
+msgstr "Банківський рахунок"
+
+#: src/paths/instance/accounts/list/Table.tsx:67
+#, fuzzy, c-format
+msgid "Add new account"
+msgstr "Зарахований банківський рахунок"
+
+#: src/paths/instance/accounts/list/Table.tsx:136
+#, fuzzy, c-format
+msgid "Wire method: Bitcoin"
+msgstr "Метод переказу"
+
+#: src/paths/instance/accounts/list/Table.tsx:145
+#, c-format
+msgid "Sewgit 1"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:148
+#, c-format
+msgid "Sewgit 2"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:180
+#, fuzzy, c-format
+msgid "Delete selected accounts from the database"
+msgstr "видалити вибраний переказ з бази даних"
+
+#: src/paths/instance/accounts/list/Table.tsx:198
+#, fuzzy, c-format
+msgid "Wire method: x-taler-bank"
+msgstr "Метод переказу"
+
+#: src/paths/instance/accounts/list/Table.tsx:207
+#, fuzzy, c-format
+msgid "Account name"
+msgstr "Номер рахунку."
+
+#: src/paths/instance/accounts/list/Table.tsx:251
+#, fuzzy, c-format
+msgid "Wire method: IBAN"
+msgstr "Метод переказу"
+
+#: src/paths/instance/accounts/list/Table.tsx:304
+#, fuzzy, c-format
+msgid "Other accounts"
+msgstr "Цільовий рахунок"
+
+#: src/paths/instance/accounts/list/Table.tsx:313
+#, c-format
+msgid "Path"
+msgstr ""
+
+#: src/paths/instance/accounts/list/Table.tsx:367
+#, fuzzy, c-format
+msgid "There is no accounts yet, add more pressing the + sign"
+msgstr "Продуктів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/accounts/list/index.tsx:77
+#, fuzzy, c-format
+msgid "You need to associate a bank account to receive revenue."
+msgstr "URI, що вказує на банківський рахунок для зарахування доходу."
+
+#: src/paths/instance/accounts/list/index.tsx:78
+#, fuzzy, c-format
+msgid "Without this the you won't be able to create new orders."
+msgstr "використовувати шаблон для створення нового замовлення"
+
+#: src/paths/instance/accounts/list/index.tsx:98
+#, fuzzy, c-format
+msgid "Bank account delete successfully"
+msgstr "продукт успішно видалено"
+
+#: src/paths/instance/accounts/list/index.tsx:103
+#, fuzzy, c-format
+msgid "Could not delete the bank account"
+msgstr "не вдалося видалити продукт"
+
+#: src/paths/instance/accounts/update/index.tsx:90
+#, fuzzy, c-format
+msgid "Could not update account"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/accounts/update/index.tsx:135
+#, fuzzy, c-format
+msgid "Could not delete account"
+msgstr "не вдалося видалити продукт"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:41
+#, c-format
+msgid "Pending KYC verification"
+msgstr "Очікування перевірки KYC"
+
+#: 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 "Exchange"
+
+#: 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 "Reason"
+msgstr "Причина"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:122
+#, c-format
+msgid "There is an anti-money laundering process pending to complete."
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:138
+#, c-format
+msgid "Pending KYC process, click here to complete"
+msgstr ""
+
+#: src/paths/instance/kyc/list/ListPage.tsx:167
+#, c-format
+msgid "Http Status"
+msgstr "HTTP статус"
+
+#: src/paths/instance/kyc/list/ListPage.tsx:197
+#, c-format
+msgid "No pending kyc verification!"
+msgstr "Немає очікуваних перевірок KYC!"
+
+#: src/components/form/InputDate.tsx:127
+#, fuzzy, c-format
+msgid "Change value to unknown date"
+msgstr "змінити значення на невідому дату"
+
+#: src/components/form/InputDate.tsx:128
+#, fuzzy, c-format
+msgid "Change value to empty"
+msgstr "змінити значення на порожнє"
+
+#: src/components/form/InputDate.tsx:140
+#, fuzzy, c-format
+msgid "Change value to never"
+msgstr "змінити значення на ніколи"
+
+#: src/components/form/InputDate.tsx:145
+#, fuzzy, c-format
+msgid "Never"
+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:62
+#, fuzzy, c-format
+msgid "Forever"
+msgstr "назавжди"
+
+#: src/components/form/InputDuration.tsx:78
+#, c-format
+msgid "%1$sM"
+msgstr "%1$sМ"
+
+#: src/components/form/InputDuration.tsx:80
+#, c-format
+msgid "%1$sY"
+msgstr "%1$sР"
+
+#: src/components/form/InputDuration.tsx:82
+#, c-format
+msgid "%1$sd"
+msgstr "%1$sдн."
+
+#: src/components/form/InputDuration.tsx:84
+#, c-format
+msgid "%1$sh"
+msgstr "%1$sг"
+
+#: src/components/form/InputDuration.tsx:86
+#, c-format
+msgid "%1$smin"
+msgstr "%1$sхв"
+
+#: src/components/form/InputDuration.tsx:88
+#, c-format
+msgid "%1$ssec"
+msgstr "%1$sсек"
+
+#: src/components/form/InputLocation.tsx:29
+#, c-format
+msgid "Country"
+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/InputSearchOnList.tsx:80
+#, c-format
+msgid "Description"
+msgstr "Опис"
+
+#: src/components/form/InputSearchOnList.tsx:106
+#, fuzzy, c-format
+msgid "Enter description or id"
+msgstr "Введіть ідентифікатор замовлення"
+
+#: src/components/form/InputSearchOnList.tsx:164
+#, fuzzy, c-format
+msgid "no match found with that description or id"
+msgstr "продукти з таким описом не знайдено"
+
+#: src/components/product/InventoryProductForm.tsx:57
+#, c-format
+msgid "You must enter a valid product identifier."
+msgstr "Ви повинні ввести дійсний ідентифікатор продукту."
+
+#: src/components/product/InventoryProductForm.tsx:65
+#, c-format
+msgid "Quantity must be greater than 0!"
+msgstr "Кількість має бути більше 0!"
+
+#: src/components/product/InventoryProductForm.tsx:77
+#, c-format
+msgid ""
+"This quantity exceeds remaining stock. Currently, only %1$s units remain "
+"unreserved in stock."
+msgstr ""
+"Ця кількість перевищує залишок на складі. Наразі на складі залишилося лише "
+"%1$s одиниць, які не зарезервовані."
+
+#: src/components/product/InventoryProductForm.tsx:100
+#, fuzzy, c-format
+msgid "Search product"
+msgstr "нова ціна для продукту"
+
+#: src/components/product/InventoryProductForm.tsx:112
+#, c-format
+msgid "Quantity"
+msgstr "Кількість"
+
+#: src/components/product/InventoryProductForm.tsx:113
+#, fuzzy, c-format
+msgid "How many products will be added"
+msgstr "скільки продуктів буде додано"
+
+#: src/components/product/InventoryProductForm.tsx:120
+#, c-format
+msgid "Add from inventory"
+msgstr "Додати зі складу"
+
+#: src/components/form/InputImage.tsx:105
+#, fuzzy, c-format
+msgid "Image must be smaller than 1 MB"
+msgstr "Зображення повинно бути меншим за 1 МБ"
+
+#: 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:47
+#, fuzzy, c-format
+msgid "Invalid"
+msgstr "недійсний"
+
+#: src/components/form/InputTaxes.tsx:66
+#, c-format
+msgid "This product has %1$s applicable taxes configured."
+msgstr ""
+
+#: src/components/form/InputTaxes.tsx:103
+#, c-format
+msgid "No taxes configured for this product."
+msgstr "Податки для цього продукту не налаштовані."
+
+#: src/components/form/InputTaxes.tsx:109
+#, c-format
+msgid "Amount"
+msgstr "Сума"
+
+#: src/components/form/InputTaxes.tsx:110
+#, c-format
+msgid ""
+"Taxes can be in currencies that differ from the main currency used by the "
+"merchant."
+msgstr ""
+"Податки можуть бути в валютах, що відрізняються від основної валюти, яку "
+"використовує продавець."
+
+#: src/components/form/InputTaxes.tsx:112
+#, c-format
+msgid ""
+"Enter currency and value separated with a colon, e.g. &quot;USD:2.3&quot;."
+msgstr ""
+"Введіть валюту та значення, розділені двокрапкою, наприклад, &quot;"
+"USD:2.3&quot;."
+
+#: src/components/form/InputTaxes.tsx:121
+#, c-format
+msgid "Legal name of the tax, e.g. VAT or import duties."
+msgstr "Офіційна назва податку, наприклад, ПДВ або імпортні мита."
+
+#: src/components/form/InputTaxes.tsx:127
+#, fuzzy, c-format
+msgid "Add tax to the tax list"
+msgstr "додати податок до списку податків"
+
+#: src/components/product/NonInventoryProductForm.tsx:71
+#, fuzzy, c-format
+msgid "Describe and add a product that is not in the inventory list"
+msgstr "опишіть і додайте продукт, якого немає в списку інвентарю"
+
+#: src/components/product/NonInventoryProductForm.tsx:74
+#, c-format
+msgid "Add custom product"
+msgstr "Додати новий продукт"
+
+#: src/components/product/NonInventoryProductForm.tsx:85
+#, c-format
+msgid "Complete information of the product"
+msgstr "Повна інформація про продукт"
+
+#: src/components/product/NonInventoryProductForm.tsx:152
+#, fuzzy, c-format
+msgid "Must be a number"
+msgstr "не є числом"
+
+#: src/components/product/NonInventoryProductForm.tsx:154
+#, fuzzy, c-format
+msgid "Must be grater than 0"
+msgstr "має бути більше 0"
+
+#: src/components/product/NonInventoryProductForm.tsx:185
+#, c-format
+msgid "Image"
+msgstr "Зображення"
+
+#: src/components/product/NonInventoryProductForm.tsx:186
+#, fuzzy, c-format
+msgid "Photo of the product."
+msgstr "фото продукту"
+
+#: src/components/product/NonInventoryProductForm.tsx:192
+#, fuzzy, c-format
+msgid "Full product description."
+msgstr "повний опис продукту"
+
+#: src/components/product/NonInventoryProductForm.tsx:196
+#, c-format
+msgid "Unit"
+msgstr "Одиниця"
+
+#: src/components/product/NonInventoryProductForm.tsx:197
+#, fuzzy, 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
+#, fuzzy, c-format
+msgid "Amount in the current currency."
+msgstr "сума в поточній валюті"
+
+#: src/components/product/NonInventoryProductForm.tsx:208
+#, fuzzy, c-format
+msgid "How many products will be added."
+msgstr "скільки продуктів буде додано"
+
+#: src/components/product/NonInventoryProductForm.tsx:211
+#, c-format
+msgid "Taxes"
+msgstr "Податки"
+
+#: src/components/product/ProductList.tsx:46
+#, fuzzy, c-format
+msgid "Unit price"
+msgstr "ціна за одиницю"
+
+#: src/components/product/ProductList.tsx:49
+#, c-format
+msgid "Total price"
+msgstr "Загальна ціна"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:162
+#, fuzzy, c-format
+msgid "Must be greater than 0"
+msgstr "має бути більше 0"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:173
+#, fuzzy, c-format
+msgid "Refund deadline can't be before pay deadline"
+msgstr "термін повернення не може бути раніше терміну оплати"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:179
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't be before refund deadline"
+msgstr "термін банківського переказу не може бути раніше терміну повернення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:188
+#, fuzzy, c-format
+msgid "Wire transfer deadline can't be before pay deadline"
+msgstr "термін банківського переказу не може бути раніше терміну оплати"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:196
+#, fuzzy, c-format
+msgid "Must have a refund deadline"
+msgstr "повинен бути встановлений термін повернення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:201
+#, fuzzy, c-format
+msgid "Auto refund can't be after refund deadline"
+msgstr "автоматичне повернення не може бути після терміну повернення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:208
+#, fuzzy, c-format
+msgid "Must be in the future"
+msgstr "повинно бути в майбутньому"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:376
+#, c-format
+msgid "Simple"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:388
+#, c-format
+msgid "Advanced"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:400
+#, c-format
+msgid "Manage products in order"
+msgstr "Керування продуктами в замовленні"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:404
+#, fuzzy, c-format
+msgid "%1$s products with a total price of %2$s."
+msgstr "оновити продукт з новим запасом і ціною"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:411
+#, c-format
+msgid "Manage list of products in the order."
+msgstr "Керування списком продуктів у замовленні."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:435
+#, c-format
+msgid "Remove this product from the order."
+msgstr "Видалити цей продукт із замовлення."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:461
+#, fuzzy, c-format
+msgid "Total product price added up"
+msgstr "загальна сума продукту"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:474
+#, c-format
+msgid "Amount to be paid by the customer"
+msgstr "Сума, яку має сплатити клієнт"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:480
+#, c-format
+msgid "Order price"
+msgstr "Ціна замовлення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:481
+#, fuzzy, c-format
+msgid "Final order price"
+msgstr "кінцева ціна замовлення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:488
+#, c-format
+msgid "Summary"
+msgstr "Підсумок"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:489
+#, c-format
+msgid "Title of the order to be shown to the customer"
+msgstr "Назва замовлення, яку буде показано клієнту"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:495
+#, c-format
+msgid "Shipping and Fulfillment"
+msgstr "Доставка та виконання"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:500
+#, c-format
+msgid "Delivery date"
+msgstr "Дата доставки"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:501
+#, c-format
+msgid "Deadline for physical delivery assured by the merchant."
+msgstr "Термін фізичної доставки, гарантований продавцем."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:506
+#, c-format
+msgid "Location"
+msgstr "Місцезнаходження"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:507
+#, fuzzy, c-format
+msgid "Address where the products will be delivered"
+msgstr "адреса, за якою будуть доставлені продукти"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:514
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL виконання"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:515
+#, c-format
+msgid "URL to which the user will be redirected after successful payment."
+msgstr "URL, на який користувача буде перенаправлено після успішної оплати."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:523
+#, c-format
+msgid "Taler payment options"
+msgstr "Опції оплати Taler"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:524
+#, c-format
+msgid "Override default Taler payment settings for this order"
+msgstr ""
+"Перевизначити стандартні налаштування оплати Taler для цього замовлення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:529
+#, fuzzy, c-format
+msgid "Payment time"
+msgstr "Тайм-аут оплати"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:535
+#, fuzzy, c-format
+msgid ""
+"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."
+msgstr ""
+"Термін, до якого клієнт повинен оплатити пропозицію, перш ніж вона "
+"закінчиться. Продукти з інвентарю будуть зарезервовані до цього терміну."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:552
+#, fuzzy, c-format
+msgid "Default"
+msgstr "Сума за замовчуванням"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:561
+#, fuzzy, c-format
+msgid "Refund time"
+msgstr "Повернення здійснено"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:569
+#, fuzzy, c-format
+msgid ""
+"Time while the order can be refunded by the merchant. Time starts after the "
+"order is created."
+msgstr "Час, до якого замовлення може бути повернене продавцем."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:594
+#, fuzzy, c-format
+msgid "Wire transfer time"
+msgstr "Ідентифікатор банківського переказу"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:602
+#, fuzzy, c-format
+msgid ""
+"Time for the exchange to make the wire transfer. Time starts after the order "
+"is created."
+msgstr "Термін, до якого обмінник повинен здійснити банківський переказ."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:628
+#, fuzzy, c-format
+msgid "Auto-refund time"
+msgstr "Затримка автоматичного повернення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:634
+#, c-format
+msgid ""
+"Time until which the wallet will automatically check for refunds without "
+"user interaction."
+msgstr ""
+"Час, до якого гаманець автоматично перевірятиме повернення коштів без "
+"взаємодії з користувачем."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:642
+#, fuzzy, c-format
+msgid "Maximum fee"
+msgstr "Максимальна комісія за переказ"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:643
+#, fuzzy, c-format
+msgid ""
+"Maximum 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:649
+#, c-format
+msgid "Create token"
+msgstr "Створити токен"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:650
+#, c-format
+msgid ""
+"If the order ID is easy to guess the token will prevent user to steal orders "
+"from others."
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:656
+#, c-format
+msgid "Minimum age required"
+msgstr "Мінімальний вік"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:657
+#, 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 ""
+"Будь-яке значення більше 0 обмежуватиме монети, які можуть бути використані "
+"для оплати цього контракту. Якщо порожнє, вікове обмеження визначатиметься "
+"продуктами"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:660
+#, c-format
+msgid "Min age defined by the producs is %1$s"
+msgstr "Мінімальний вік, визначений продуктами, становить %1$s"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:661
+#, c-format
+msgid "No product with age restriction in this order"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:671
+#, c-format
+msgid "Additional information"
+msgstr "Додаткова інформація"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:672
+#, c-format
+msgid "Custom information to be included in the contract for this order."
+msgstr ""
+"Спеціальна інформація, яка буде включена в контракт для цього замовлення."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:681
+#, c-format
+msgid "You must enter a value in JavaScript Object Notation (JSON)."
+msgstr ""
+"Ви повинні ввести значення у форматі JavaScript Object Notation (JSON)."
+
+#: src/paths/instance/orders/create/CreatePage.tsx:707
+#, fuzzy, c-format
+msgid "Custom field name"
+msgstr "Назва будинку"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:793
+#, c-format
+msgid "Disabled"
+msgstr ""
+
+#: src/paths/instance/orders/create/CreatePage.tsx:796
+#, fuzzy, c-format
+msgid "No deadline"
+msgstr "Термін повернення"
+
+#: src/paths/instance/orders/create/CreatePage.tsx:797
+#, c-format
+msgid "Deadline at %1$s"
+msgstr ""
+
+#: src/paths/instance/orders/create/index.tsx:109
+#, fuzzy, c-format
+msgid "Could not create order"
+msgstr "не вдалося створити резерв"
+
+#: src/paths/instance/orders/create/index.tsx:111
+#, c-format
+msgid "No exchange would accept a payment because of KYC requirements."
+msgstr ""
+
+#: src/paths/instance/orders/create/index.tsx:129
+#, c-format
+msgid "No more stock for product with id \"%1$s\"."
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:77
+#, c-format
+msgid "Orders"
+msgstr "Замовлення"
+
+#: src/paths/instance/orders/list/Table.tsx:83
+#, fuzzy, c-format
+msgid "Create order"
+msgstr "створити замовлення"
+
+#: src/paths/instance/orders/list/Table.tsx:140
+#, c-format
+msgid "Load first page"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:147
+#, c-format
+msgid "Date"
+msgstr "Дата"
+
+#: src/paths/instance/orders/list/Table.tsx:193
+#, c-format
+msgid "Refund"
+msgstr "Повернення"
+
+#: src/paths/instance/orders/list/Table.tsx:202
+#, c-format
+msgid "copy url"
+msgstr "скопіювати url"
+
+#: src/paths/instance/orders/list/Table.tsx:214
+#, fuzzy, c-format
+msgid "Load more orders after the last one"
+msgstr "завантажити більше переказів після останнього"
+
+#: src/paths/instance/orders/list/Table.tsx:216
+#, c-format
+msgid "Load next page"
+msgstr ""
+
+#: src/paths/instance/orders/list/Table.tsx:233
+#, c-format
+msgid "No orders have been found matching your query!"
+msgstr "Замовлення, що відповідають вашому запиту, не знайдено!"
+
+#: src/paths/instance/orders/list/Table.tsx:280
+#, fuzzy, c-format
+msgid "Duplicated"
+msgstr "дубльовано"
+
+#: src/paths/instance/orders/list/Table.tsx:293
+#, fuzzy, c-format
+msgid "This value exceed the refundable amount"
+msgstr "ця сума перевищує суму, що підлягає поверненню"
+
+#: src/paths/instance/orders/list/Table.tsx:381
+#, fuzzy, c-format
+msgid "Amount to be refunded"
+msgstr "сума до повернення"
+
+#: src/paths/instance/orders/list/Table.tsx:383
+#, c-format
+msgid "Max refundable:"
+msgstr "Макс. сума для повернення:"
+
+#: src/paths/instance/orders/list/Table.tsx:391
+#, fuzzy, c-format
+msgid "Requested by the customer"
+msgstr "запитано клієнтом"
+
+#: src/paths/instance/orders/list/Table.tsx:392
+#, fuzzy, c-format
+msgid "Other"
+msgstr "інше"
+
+#: src/paths/instance/orders/list/Table.tsx:395
+#, fuzzy, c-format
+msgid "Why this order is being refunded"
+msgstr "чому це замовлення повертається"
+
+#: src/paths/instance/orders/list/Table.tsx:401
+#, fuzzy, c-format
+msgid "More information to give context"
+msgstr "додаткова інформація для надання контексту"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:72
+#, c-format
+msgid "Contract Terms"
+msgstr "Умови контракту"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:78
+#, fuzzy, c-format
+msgid "Human-readable description of the whole purchase"
+msgstr "читабельний опис всієї покупки"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:84
+#, fuzzy, c-format
+msgid "Total price for the transaction"
+msgstr "загальна ціна за транзакцію"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:91
+#, c-format
+msgid "URL for this purchase"
+msgstr "URL для цієї покупки"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:97
+#, c-format
+msgid "Max fee"
+msgstr "Максимальна комісія"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:98
+#, fuzzy, c-format
+msgid "Maximum total deposit fee accepted by the merchant for this contract"
+msgstr ""
+"максимальна загальна комісія за депозит, прийнята продавцем для цього "
+"контракту"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:103
+#, c-format
+msgid "Created at"
+msgstr "Створено о"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:104
+#, fuzzy, c-format
+msgid "Time when this contract was generated"
+msgstr "час, коли цей контракт було згенеровано"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:109
+#, c-format
+msgid "Refund deadline"
+msgstr "Термін повернення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:110
+#, fuzzy, c-format
+msgid "After this deadline has passed no refunds will be accepted"
+msgstr "після цього терміну повернення не приймаються"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:115
+#, c-format
+msgid "Payment deadline"
+msgstr "Термін оплати"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:116
+#, fuzzy, c-format
+msgid ""
+"After this deadline, the merchant won't accept payments for the contract"
+msgstr "після цього терміну продавець не прийматиме платежі за контрактом"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:121
+#, c-format
+msgid "Wire transfer deadline"
+msgstr "Термін банківського переказу"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:122
+#, fuzzy, c-format
+msgid "Transfer deadline for the exchange"
+msgstr "термін переказу для обмінника"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:128
+#, fuzzy, c-format
+msgid "Time indicating when the order should be delivered"
+msgstr "час, що вказує, коли замовлення має бути доставлене"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:134
+#, fuzzy, c-format
+msgid "Where the order will be delivered"
+msgstr "куди буде доставлене замовлення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:142
+#, c-format
+msgid "Auto-refund delay"
+msgstr "Затримка автоматичного повернення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:143
+#, fuzzy, 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:148
+#, c-format
+msgid "Extra info"
+msgstr "Додаткова інформація"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:149
+#, fuzzy, c-format
+msgid "Extra data that is only interpreted by the merchant frontend"
+msgstr "додаткові дані, які інтерпретуються лише фронтендом продавця"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:222
+#, c-format
+msgid "Order"
+msgstr "Замовлення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:224
+#, fuzzy, c-format
+msgid "Claimed"
+msgstr "отримано"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:251
+#, fuzzy, c-format
+msgid "Claimed at"
+msgstr "отримано о"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:273
+#, c-format
+msgid "Timeline"
+msgstr "Хронологія"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:279
+#, c-format
+msgid "Payment details"
+msgstr "Деталі оплати"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:299
+#, c-format
+msgid "Order status"
+msgstr "Статус замовлення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:309
+#, c-format
+msgid "Product list"
+msgstr "Список продуктів"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:461
+#, c-format
+msgid "Paid"
+msgstr "Оплачено"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:465
+#, fuzzy, c-format
+msgid "Wired"
+msgstr "перераховано"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:470
+#, c-format
+msgid "Refunded"
+msgstr "Повернено"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:490
+#, fuzzy, c-format
+msgid "Refund order"
+msgstr "замовлення на повернення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:491
+#, fuzzy, c-format
+msgid "Not refundable"
+msgstr "не підлягає поверненню"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:521
+#, c-format
+msgid "Next event in"
+msgstr ""
+
+#: src/paths/instance/orders/details/DetailPage.tsx:557
+#, c-format
+msgid "Refunded amount"
+msgstr "Повернена сума"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:564
+#, c-format
+msgid "Refund taken"
+msgstr "Повернення здійснено"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:574
+#, c-format
+msgid "Status URL"
+msgstr "URL статусу"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:587
+#, c-format
+msgid "Refund URI"
+msgstr "URI повернення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:641
+#, fuzzy, c-format
+msgid "Unpaid"
+msgstr "неоплачено"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:659
+#, fuzzy, c-format
+msgid "Pay at"
+msgstr "оплачено о"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:712
+#, c-format
+msgid "Order status URL"
+msgstr "URL статусу замовлення"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:716
+#, c-format
+msgid "Payment URI"
+msgstr "URI оплати"
+
+#: src/paths/instance/orders/details/DetailPage.tsx:745
+#, c-format
+msgid ""
+"Unknown order status. This is an error, please contact the administrator."
+msgstr ""
+"Невідомий статус замовлення. Це помилка, будь ласка, зв'яжіться з "
+"адміністратором."
+
+#: src/paths/instance/orders/details/DetailPage.tsx:772
+#, c-format
+msgid "Back"
+msgstr "Назад"
+
+#: src/paths/instance/orders/details/index.tsx:88
+#, fuzzy, c-format
+msgid "Refund created successfully"
+msgstr "повернення успішно створено"
+
+#: src/paths/instance/orders/details/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not create the refund"
+msgstr "не вдалося створити повернення"
+
+#: src/paths/instance/orders/details/index.tsx:97
+#, c-format
+msgid "There are pending KYC requirements."
+msgstr ""
+
+#: src/components/form/JumpToElementById.tsx:39
+#, c-format
+msgid "Missing id"
+msgstr ""
+
+#: src/components/form/JumpToElementById.tsx:48
+#, fuzzy, c-format
+msgid "Not found"
+msgstr "замовлення не знайдено"
+
+#: src/paths/instance/orders/list/ListPage.tsx:83
+#, fuzzy, c-format
+msgid "Select date to show nearby orders"
+msgstr "виберіть дату, щоб показати найближчі замовлення"
+
+#: src/paths/instance/orders/list/ListPage.tsx:96
+#, fuzzy, c-format
+msgid "Only show paid orders"
+msgstr "показувати лише оплачені замовлення"
+
+#: src/paths/instance/orders/list/ListPage.tsx:99
+#, c-format
+msgid "New"
+msgstr "Новий"
+
+#: src/paths/instance/orders/list/ListPage.tsx:116
+#, fuzzy, c-format
+msgid "Only show orders with refunds"
+msgstr "показувати лише замовлення з поверненнями"
+
+#: src/paths/instance/orders/list/ListPage.tsx:126
+#, fuzzy, 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:129
+#, c-format
+msgid "Not wired"
+msgstr "Не перераховано"
+
+#: src/paths/instance/orders/list/ListPage.tsx:139
+#, fuzzy, c-format
+msgid "Completed"
+msgstr "Видалено"
+
+#: src/paths/instance/orders/list/ListPage.tsx:146
+#, fuzzy, c-format
+msgid "Remove all filters"
+msgstr "видалити всі фільтри"
+
+#: src/paths/instance/orders/list/ListPage.tsx:164
+#, fuzzy, c-format
+msgid "Clear date filter"
+msgstr "очистити фільтр дати"
+
+#: src/paths/instance/orders/list/ListPage.tsx:178
+#, c-format
+msgid "Jump to date (%1$s)"
+msgstr ""
+
+#: src/paths/instance/orders/list/index.tsx:113
+#, fuzzy, c-format
+msgid "Jump to order with the given product ID"
+msgstr "перейти до замовлення з зазначеним ідентифікатором"
+
+#: src/paths/instance/orders/list/index.tsx:114
+#, fuzzy, c-format
+msgid "Order id"
+msgstr "ідентифікатор замовлення"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:61
+#, c-format
+msgid "Invalid. Only characters and numbers"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:67
+#, fuzzy, c-format
+msgid "Just letters and numbers from 2 to 7"
+msgstr "лише літери та цифри від 2 до 7"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:69
+#, fuzzy, c-format
+msgid "Size of the key must be 32"
+msgstr "розмір ключа повинен бути 32"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:99
+#, c-format
+msgid "Internal id on the system"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:104
+#, c-format
+msgid "Useful to identify the device physically"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:108
+#, c-format
+msgid "Verification algorithm"
+msgstr "Алгоритм перевірки"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:109
+#, c-format
+msgid "Algorithm to use to verify transaction in offline mode"
+msgstr "Алгоритм для використання для перевірки транзакції в офлайн-режимі"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:119
+#, c-format
+msgid "Device key"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:121
+#, c-format
+msgid "Be sure to be very hard to guess or use the random generator"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:122
+#, c-format
+msgid "Your device need to have exactly the same value"
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:138
+#, fuzzy, c-format
+msgid "Generate random secret key"
+msgstr "згенерувати випадковий секретний ключ"
+
+#: src/paths/instance/otp_devices/create/CreatePage.tsx:148
+#, fuzzy, c-format
+msgid "Random"
+msgstr "випадковий"
+
+#: src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx:44
+#, c-format
+msgid ""
+"You can scan the next QR code with your device or save the key before "
+"continuing."
+msgstr ""
+
+#: src/paths/instance/otp_devices/create/index.tsx:60
+#, fuzzy, c-format
+msgid "Device added successfully"
+msgstr "повернення успішно створено"
+
+#: src/paths/instance/otp_devices/create/index.tsx:66
+#, fuzzy, c-format
+msgid "Could not add device"
+msgstr "не вдалося створити резерв"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:57
+#, c-format
+msgid "OTP Devices"
+msgstr ""
+
+#: src/paths/instance/otp_devices/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Add new devices"
+msgstr "додати новий резерв"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:117
+#, fuzzy, c-format
+msgid "Load more devices before the first one"
+msgstr "завантажити більше шаблонів до першого"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:155
+#, fuzzy, c-format
+msgid "Delete selected devices from the database"
+msgstr "видалити вибраний резерв з бази даних"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:170
+#, fuzzy, c-format
+msgid "Load more devices after the last one"
+msgstr "завантажити більше шаблонів після останнього"
+
+#: src/paths/instance/otp_devices/list/Table.tsx:190
+#, fuzzy, c-format
+msgid "There is no devices yet, add more pressing the + sign"
+msgstr "Ще немає інстанцій, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/otp_devices/list/index.tsx:90
+#, fuzzy, c-format
+msgid "Device delete successfully"
+msgstr "шаблон успішно видалено"
+
+#: src/paths/instance/otp_devices/list/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not delete the device"
+msgstr "не вдалося видалити продукт"
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:64
+#, c-format
+msgid "Device:"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:100
+#, fuzzy, c-format
+msgid "Not modified"
+msgstr "Не перераховано"
+
+#: src/paths/instance/otp_devices/update/UpdatePage.tsx:130
+#, c-format
+msgid "Change key"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/index.tsx:119
+#, fuzzy, c-format
+msgid "Could not update template"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/otp_devices/update/index.tsx:121
+#, c-format
+msgid "Template id is unknown"
+msgstr ""
+
+#: src/paths/instance/otp_devices/update/index.tsx:129
+#, c-format
+msgid ""
+"The provided information is inconsistent with the current state of the "
+"template"
+msgstr ""
+
+#: src/components/form/InputStock.tsx:99
+#, fuzzy, 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
+#, fuzzy, 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
+#, fuzzy, c-format
+msgid "Lost can't be greater than current and incoming (max %1$s)"
+msgstr "втрати не можуть бути більшими за поточні та прибуваючі (макс %1$s)"
+
+#: src/components/form/InputStock.tsx:169
+#, c-format
+msgid "Incoming"
+msgstr "Прибуття"
+
+#: src/components/form/InputStock.tsx:170
+#, c-format
+msgid "Lost"
+msgstr "Втрачено"
+
+#: src/components/form/InputStock.tsx:185
+#, c-format
+msgid "Current"
+msgstr "Поточний"
+
+#: src/components/form/InputStock.tsx:189
+#, fuzzy, c-format
+msgid "Remove stock control for this product"
+msgstr "видалити контроль запасів для цього продукту"
+
+#: src/components/form/InputStock.tsx:195
+#, c-format
+msgid "without stock"
+msgstr "без запасу"
+
+#: src/components/form/InputStock.tsx:204
+#, c-format
+msgid "Next restock"
+msgstr "Наступне поповнення"
+
+#: src/components/form/InputStock.tsx:208
+#, fuzzy, c-format
+msgid "Warehouse address"
+msgstr "Адреса рахунку"
+
+#: src/components/form/InputArray.tsx:118
+#, fuzzy, c-format
+msgid "Add element to the list"
+msgstr "додати елемент до списку"
+
+#: src/components/product/ProductForm.tsx:120
+#, fuzzy, c-format
+msgid "Invalid amount"
+msgstr "Фіксована сума"
+
+#: src/components/product/ProductForm.tsx:181
+#, fuzzy, c-format
+msgid "Product identification to use in URLs (for internal use only)."
+msgstr ""
+"ідентифікація продукту для використання в URL (тільки для внутрішнього "
+"використання)"
+
+#: src/components/product/ProductForm.tsx:187
+#, fuzzy, c-format
+msgid "Illustration of the product for customers."
+msgstr "ілюстрація продукту для клієнтів"
+
+#: src/components/product/ProductForm.tsx:193
+#, fuzzy, c-format
+msgid "Product description for customers."
+msgstr "опис продукту для клієнтів"
+
+#: src/components/product/ProductForm.tsx:197
+#, fuzzy, c-format
+msgid "Age restriction"
+msgstr "Обмежений за віком"
+
+#: src/components/product/ProductForm.tsx:198
+#, fuzzy, c-format
+msgid "Is this product restricted for customer below certain age?"
+msgstr "цей продукт обмежений для клієнтів молодше певного віку?"
+
+#: src/components/product/ProductForm.tsx:199
+#, fuzzy, c-format
+msgid "Minimum age of the customer"
+msgstr "Мінімальний вік"
+
+#: src/components/product/ProductForm.tsx:203
+#, fuzzy, c-format
+msgid "Unit name"
+msgstr "Одиниця"
+
+#: src/components/product/ProductForm.tsx:204
+#, fuzzy, c-format
+msgid ""
+"Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 "
+"items, 5 meters) for customers."
+msgstr ""
+"одиниця, що описує кількість проданого продукту (наприклад, 2 кілограми, 5 "
+"літрів, 3 предмети, 5 метрів) для клієнтів"
+
+#: src/components/product/ProductForm.tsx:205
+#, c-format
+msgid "Example: kg, items or liters"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:209
+#, c-format
+msgid "Price per unit"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:210
+#, fuzzy, c-format
+msgid ""
+"Sale price for customers, including taxes, for above units of the product."
+msgstr ""
+"ціна продажу для клієнтів, включаючи податки, за вищезазначені одиниці "
+"продукту"
+
+#: src/components/product/ProductForm.tsx:214
+#, c-format
+msgid "Stock"
+msgstr "Запас"
+
+#: src/components/product/ProductForm.tsx:216
+#, fuzzy, c-format
+msgid "Inventory for products with finite supply (for internal use only)."
+msgstr ""
+"інвентаризація продукту для продуктів з обмеженим запасом (тільки для "
+"внутрішнього використання)"
+
+#: src/components/product/ProductForm.tsx:221
+#, fuzzy, c-format
+msgid "Taxes included in the product price, exposed to customers."
+msgstr "податки, включені в ціну продукту, показані клієнтам"
+
+#: src/components/product/ProductForm.tsx:225
+#, c-format
+msgid "Categories"
+msgstr ""
+
+#: src/components/product/ProductForm.tsx:231
+#, fuzzy, c-format
+msgid "Search by category description or id"
+msgstr "шукати продукти за їхнім описом або ідентифікатором"
+
+#: src/components/product/ProductForm.tsx:232
+#, fuzzy, c-format
+msgid "Categories where this product will be listed on."
+msgstr "адреса, за якою будуть доставлені продукти"
+
+#: src/paths/instance/products/create/index.tsx:52
+#, fuzzy, c-format
+msgid "Product created successfully"
+msgstr "продукт успішно оновлено"
+
+#: src/paths/instance/products/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create product"
+msgstr "не вдалося створити продукт"
+
+#: src/paths/instance/products/list/Table.tsx:73
+#, c-format
+msgid "Inventory"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:78
+#, fuzzy, c-format
+msgid "Add product to inventory"
+msgstr "додати продукт до інвентарю"
+
+#: src/paths/instance/products/list/Table.tsx:160
+#, c-format
+msgid "Sales"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:166
+#, c-format
+msgid "Sold"
+msgstr "Продано"
+
+#: src/paths/instance/products/list/Table.tsx:230
+#, c-format
+msgid "Free"
+msgstr ""
+
+#: src/paths/instance/products/list/Table.tsx:271
+#, fuzzy, c-format
+msgid "Go to product update page"
+msgstr "перейти на сторінку оновлення продукту"
+
+#: src/paths/instance/products/list/Table.tsx:278
+#, c-format
+msgid "Update"
+msgstr "Оновити"
+
+#: src/paths/instance/products/list/Table.tsx:283
+#, fuzzy, c-format
+msgid "Remove this product from the database"
+msgstr "видалити цей продукт з бази даних"
+
+#: src/paths/instance/products/list/Table.tsx:318
+#, fuzzy, c-format
+msgid "Load more products after the last one"
+msgstr "завантажити більше шаблонів після останнього"
+
+#: src/paths/instance/products/list/Table.tsx:361
+#, fuzzy, c-format
+msgid "Update the product with new price"
+msgstr "оновити продукт з новою ціною"
+
+#: src/paths/instance/products/list/Table.tsx:373
+#, fuzzy, c-format
+msgid "Update product with new price"
+msgstr "оновити продукт з новою ціною"
+
+#: src/paths/instance/products/list/Table.tsx:384
+#, fuzzy, c-format
+msgid "Confirm update"
+msgstr "Підтверджено"
+
+#: src/paths/instance/products/list/Table.tsx:431
+#, fuzzy, c-format
+msgid "Add more elements to the inventory"
+msgstr "додати більше елементів до інвентарю"
+
+#: src/paths/instance/products/list/Table.tsx:436
+#, fuzzy, c-format
+msgid "Report elements lost in the inventory"
+msgstr "повідомити про втрату елементів в інвентарі"
+
+#: src/paths/instance/products/list/Table.tsx:441
+#, fuzzy, c-format
+msgid "New price for the product"
+msgstr "нова ціна для продукту"
+
+#: src/paths/instance/products/list/Table.tsx:453
+#, fuzzy, c-format
+msgid "The are value with errors"
+msgstr "є значення з помилками"
+
+#: src/paths/instance/products/list/Table.tsx:454
+#, fuzzy, c-format
+msgid "Update product with new stock and price"
+msgstr "оновити продукт з новим запасом і ціною"
+
+#: src/paths/instance/products/list/Table.tsx:495
+#, c-format
+msgid "There is no products yet, add more pressing the + sign"
+msgstr "Продуктів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/products/list/index.tsx:86
+#, fuzzy, c-format
+msgid "Jump to product with the given product ID"
+msgstr "перейти до замовлення з зазначеним ідентифікатором"
+
+#: src/paths/instance/products/list/index.tsx:87
+#, c-format
+msgid "Product id"
+msgstr "Ідентифікатор продукту"
+
+#: src/paths/instance/products/list/index.tsx:104
+#, fuzzy, c-format
+msgid "Product updated successfully"
+msgstr "продукт успішно оновлено"
+
+#: src/paths/instance/products/list/index.tsx:109
+#, fuzzy, c-format
+msgid "Could not update the product"
+msgstr "не вдалося оновити продукт"
+
+#: src/paths/instance/products/list/index.tsx:144
+#, fuzzy, c-format
+msgid "Product \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/products/list/index.tsx:149
+#, fuzzy, c-format
+msgid "Could not delete the product"
+msgstr "не вдалося видалити продукт"
+
+#: src/paths/instance/products/list/index.tsx:165
+#, c-format
+msgid ""
+"If you delete the product named %1$s (ID: %2$s ), the stock and related "
+"information will be lost"
+msgstr ""
+
+#: src/paths/instance/products/list/index.tsx:173
+#, c-format
+msgid "Deleting an product can't be undone."
+msgstr ""
+
+#: src/paths/instance/products/update/UpdatePage.tsx:56
+#, c-format
+msgid "Product id:"
+msgstr "Ідентифікатор продукту:"
+
+#: src/paths/instance/products/update/index.tsx:85
+#, fuzzy, c-format
+msgid "Product (ID: %1$s) has been updated"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/products/update/index.tsx:91
+#, fuzzy, c-format
+msgid "Could not update product"
+msgstr "не вдалося оновити продукт"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:96
+#, c-format
+msgid "Invalid. only characters and numbers"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:112
+#, fuzzy, c-format
+msgid "Must be greater that 0"
+msgstr "має бути більше 0"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:119
+#, fuzzy, c-format
+msgid "To short"
+msgstr "занадто короткий"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:192
+#, c-format
+msgid "Identifier"
+msgstr "Ідентифікатор"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:193
+#, c-format
+msgid "Name of the template in URLs."
+msgstr "Назва шаблону в URL."
+
+#: src/paths/instance/templates/create/CreatePage.tsx:199
+#, c-format
+msgid "Describe what this template stands for"
+msgstr "Опишіть, що представляє цей шаблон"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:206
+#, c-format
+msgid "If specified, this template will create order with the same summary"
+msgstr "Якщо вказано, цей шаблон створить замовлення з однаковим підсумком"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:210
+#, c-format
+msgid "Summary is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:211
+#, c-format
+msgid "Allow the user to change the summary."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:217
+#, c-format
+msgid "If specified, this template will create order with the same price"
+msgstr "Якщо вказано, цей шаблон створить замовлення з однаковою ціною"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:221
+#, fuzzy, c-format
+msgid "Amount is editable"
+msgstr "Зарахована сума"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:222
+#, c-format
+msgid "Allow the user to select the amount to pay."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:229
+#, c-format
+msgid "Currency is editable"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:230
+#, c-format
+msgid "Allow the user to change currency."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:232
+#, c-format
+msgid "Supported currencies"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:233
+#, c-format
+msgid "Supported currencies: %1$s"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:241
+#, c-format
+msgid "Minimum age"
+msgstr "Мінімальний вік"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:243
+#, c-format
+msgid "Is this contract restricted to some age?"
+msgstr "Чи обмежений цей контракт за віком?"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:247
+#, c-format
+msgid "Payment timeout"
+msgstr "Тайм-аут оплати"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:249
+#, 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:254
+#, c-format
+msgid "OTP device"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:255
+#, fuzzy, c-format
+msgid "Use to verify transaction while offline."
+msgstr "Алгоритм для використання для перевірки транзакції в офлайн-режимі"
+
+#: src/paths/instance/templates/create/CreatePage.tsx:257
+#, c-format
+msgid "No OTP device."
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:259
+#, c-format
+msgid "Add one first"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:272
+#, c-format
+msgid "No device"
+msgstr ""
+
+#: src/paths/instance/templates/create/CreatePage.tsx:276
+#, fuzzy, c-format
+msgid "Use to verify transaction in offline mode."
+msgstr "Алгоритм для використання для перевірки транзакції в офлайн-режимі"
+
+#: src/paths/instance/templates/create/index.tsx:52
+#, c-format
+msgid "Template has been created"
+msgstr ""
+
+#: src/paths/instance/templates/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create template"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/templates/list/Table.tsx:61
+#, c-format
+msgid "Templates"
+msgstr "Шаблони"
+
+#: src/paths/instance/templates/list/Table.tsx:66
+#, fuzzy, c-format
+msgid "Add new templates"
+msgstr "додати нові шаблони"
+
+#: src/paths/instance/templates/list/Table.tsx:127
+#, fuzzy, c-format
+msgid "Load more templates before the first one"
+msgstr "завантажити більше шаблонів до першого"
+
+#: src/paths/instance/templates/list/Table.tsx:165
+#, fuzzy, c-format
+msgid "Delete selected templates from the database"
+msgstr "видалити вибрані шаблони з бази даних"
+
+#: src/paths/instance/templates/list/Table.tsx:172
+#, fuzzy, c-format
+msgid "Use template to create new order"
+msgstr "використовувати шаблон для створення нового замовлення"
+
+#: src/paths/instance/templates/list/Table.tsx:175
+#, fuzzy, c-format
+msgid "Use template"
+msgstr "додати нові шаблони"
+
+#: src/paths/instance/templates/list/Table.tsx:179
+#, fuzzy, c-format
+msgid "Create qr code for the template"
+msgstr "створити QR-код для шаблону"
+
+#: src/paths/instance/templates/list/Table.tsx:194
+#, fuzzy, c-format
+msgid "Load more templates after the last one"
+msgstr "завантажити більше шаблонів після останнього"
+
+#: src/paths/instance/templates/list/Table.tsx:214
+#, c-format
+msgid "There is no templates yet, add more pressing the + sign"
+msgstr "Шаблонів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/templates/list/index.tsx:91
+#, fuzzy, c-format
+msgid "Jump to template with the given template ID"
+msgstr "перейти до замовлення з зазначеним ідентифікатором"
+
+#: src/paths/instance/templates/list/index.tsx:92
+#, c-format
+msgid "Template identification"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:132
+#, fuzzy, c-format
+msgid "Template \"%1$s\" (ID: %2$s) has been deleted"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/templates/list/index.tsx:137
+#, fuzzy, c-format
+msgid "Failed to delete template"
+msgstr "Не вдалося видалити інстанцію"
+
+#: src/paths/instance/templates/list/index.tsx:153
+#, c-format
+msgid "If you delete the template %1$s (ID: %2$s) you may loose information"
+msgstr ""
+
+#: src/paths/instance/templates/list/index.tsx:160
+#, fuzzy, c-format
+msgid "Deleting an template"
+msgstr "завантажити новіші шаблони"
+
+#: src/paths/instance/templates/list/index.tsx:162
+#, fuzzy, c-format
+msgid "can't be undone"
+msgstr "не може бути порожнім"
+
+#: src/paths/instance/templates/qr/QrPage.tsx:77
+#, c-format
+msgid "Print"
+msgstr "Друк"
+
+#: src/paths/instance/templates/update/UpdatePage.tsx:135
+#, fuzzy, c-format
+msgid "Too short"
+msgstr "занадто короткий"
+
+#: src/paths/instance/templates/update/index.tsx:90
+#, fuzzy, c-format
+msgid "Template (ID: %1$s) has been updated"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/templates/use/UsePage.tsx:58
+#, c-format
+msgid "Amount is required"
+msgstr "Сума обов'язкова"
+
+#: src/paths/instance/templates/use/UsePage.tsx:59
+#, 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:125
+#, fuzzy, c-format
+msgid "Could not create order from template"
+msgstr "не вдалося створити замовлення з шаблону"
+
+#: src/paths/instance/token/DetailPage.tsx:57
+#, fuzzy, c-format
+msgid "You need your access token to perform the operation"
+msgstr "Ви встановлюєте токен доступу для нової інстанції"
+
+#: src/paths/instance/token/DetailPage.tsx:74
+#, fuzzy, c-format
+msgid "You are updating the access token from instance with id \"%1$s\""
+msgstr "Ви оновлюєте токен доступу з інстанції з ідентифікатором %1$s"
+
+#: src/paths/instance/token/DetailPage.tsx:105
+#, c-format
+msgid "This instance doesn't have authentication token."
+msgstr ""
+
+#: src/paths/instance/token/DetailPage.tsx:106
+#, c-format
+msgid "You can leave it empty if there is another layer of security."
+msgstr ""
+
+#: src/paths/instance/token/DetailPage.tsx:121
+#, fuzzy, c-format
+msgid "Current access token"
+msgstr "Встановити токен доступу"
+
+#: src/paths/instance/token/DetailPage.tsx:126
+#, fuzzy, c-format
+msgid "Clearing the access token will mean public access to the instance."
+msgstr "Видалення токена доступу означатиме публічний доступ до системи"
+
+#: src/paths/instance/token/DetailPage.tsx:142
+#, fuzzy, c-format
+msgid "Clear token"
+msgstr "Створити токен"
+
+#: src/paths/instance/token/DetailPage.tsx:177
+#, fuzzy, c-format
+msgid "Confirm change"
+msgstr "Підтверджено"
+
+#: src/paths/instance/token/index.tsx:83
+#, fuzzy, c-format
+msgid "Failed to clear token"
+msgstr "Не вдалося створити інстанцію"
+
+#: src/paths/instance/token/index.tsx:109
+#, fuzzy, c-format
+msgid "Failed to set new token"
+msgstr "Не вдалося видалити інстанцію"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:96
+#, c-format
+msgid "Slug"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:97
+#, fuzzy, c-format
+msgid "Token family slug to use in URLs (for internal use only)"
+msgstr ""
+"ідентифікація продукту для використання в URL (тільки для внутрішнього "
+"використання)"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:101
+#, c-format
+msgid "Kind"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:102
+#, c-format
+msgid "Token family kind"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:109
+#, c-format
+msgid "User-readable token family name"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:115
+#, fuzzy, c-format
+msgid "Token family description for customers"
+msgstr "опис продукту для клієнтів"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:119
+#, fuzzy, c-format
+msgid "Valid After"
+msgstr "Дійсний до"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:120
+#, c-format
+msgid "Token family can issue tokens after this date"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:125
+#, fuzzy, c-format
+msgid "Valid Before"
+msgstr "недійсний формат"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:126
+#, c-format
+msgid "Token family can issue tokens until this date"
+msgstr ""
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:131
+#, fuzzy, c-format
+msgid "Duration"
+msgstr "Термін дії"
+
+#: src/components/tokenfamily/TokenFamilyForm.tsx:132
+#, c-format
+msgid "Validity duration of a issued token"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/create/index.tsx:51
+#, fuzzy, c-format
+msgid "Token familty created successfully"
+msgstr "повернення успішно створено"
+
+#: src/paths/instance/tokenfamilies/create/index.tsx:57
+#, fuzzy, c-format
+msgid "Could not create token family"
+msgstr "не вдалося створити чайові"
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:60
+#, c-format
+msgid "Token Families"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:65
+#, c-format
+msgid "Add token family"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:192
+#, fuzzy, c-format
+msgid "Go to token family update page"
+msgstr "перейти на сторінку оновлення продукту"
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:204
+#, fuzzy, c-format
+msgid "Remove this token family from the database"
+msgstr "видалити цей продукт з бази даних"
+
+#: src/paths/instance/tokenfamilies/list/Table.tsx:237
+#, fuzzy, c-format
+msgid ""
+"There are no token families yet, add the first one by pressing the + sign."
+msgstr "Шаблонів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:91
+#, fuzzy, c-format
+msgid "Token family updated successfully"
+msgstr "продукт успішно оновлено"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:96
+#, fuzzy, c-format
+msgid "Could not update the token family"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:129
+#, fuzzy, c-format
+msgid "Token family \"%1$s\" (SLUG: %2$s) has been deleted"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:134
+#, fuzzy, c-format
+msgid "Failed to delete token family"
+msgstr "Не вдалося видалити інстанцію"
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:150
+#, c-format
+msgid ""
+"If you delete the %1$s token family (Slug: %2$s), all issued tokens will "
+"become invalid."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/list/index.tsx:157
+#, c-format
+msgid "Deleting a token family %1$s ."
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/UpdatePage.tsx:87
+#, c-format
+msgid "Token Family: %1$s"
+msgstr ""
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:100
+#, fuzzy, c-format
+msgid "Token familty updated successfully"
+msgstr "продукт успішно оновлено"
+
+#: src/paths/instance/tokenfamilies/update/index.tsx:106
+#, fuzzy, c-format
+msgid "Could not update token family"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:62
+#, fuzzy, c-format
+msgid "Check the id, does not look valid"
+msgstr "перевірте ідентифікатор, він виглядає недійсним"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:64
+#, fuzzy, c-format
+msgid "Must have 52 characters, current %1$s"
+msgstr "повинно бути 52 символи, поточний %1$s"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:71
+#, c-format
+msgid "URL doesn't have the right format"
+msgstr "URL має неправильний формат"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:95
+#, c-format
+msgid "Credited bank account"
+msgstr "Зарахований банківський рахунок"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:97
+#, c-format
+msgid "Select one account"
+msgstr "Виберіть один рахунок"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:98
+#, c-format
+msgid "Bank account of the merchant where the payment was received"
+msgstr "Банківський рахунок продавця, на який було отримано платіж"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:102
+#, c-format
+msgid "Wire transfer ID"
+msgstr "Ідентифікатор банківського переказу"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:104
+#, fuzzy, c-format
+msgid ""
+"Unique identifier of the wire transfer used by the exchange, must be 52 "
+"characters long"
+msgstr ""
+"унікальний ідентифікатор банківського переказу, що використовується "
+"обмінником, має бути довжиною 52 символи"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:108
+#, c-format
+msgid "Exchange URL"
+msgstr "URL обмінника"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:109
+#, c-format
+msgid ""
+"Base URL of the exchange that made the transfer, should have been in the "
+"wire transfer subject"
+msgstr ""
+"Основний URL обмінника, який здійснив переказ, має бути в призначенні "
+"банківського переказу"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:114
+#, c-format
+msgid "Amount credited"
+msgstr "Зарахована сума"
+
+#: src/paths/instance/transfers/create/CreatePage.tsx:115
+#, c-format
+msgid "Actual amount that was wired to the merchant's bank account"
+msgstr "Фактична сума, що була переказана на банківський рахунок продавця"
+
+#: src/paths/instance/transfers/create/index.tsx:62
+#, fuzzy, c-format
+msgid "Wire transfer informed successfully"
+msgstr "повернення успішно створено"
+
+#: src/paths/instance/transfers/create/index.tsx:68
+#, fuzzy, c-format
+msgid "Could not inform transfer"
+msgstr "не вдалося повідомити про переказ"
+
+#: src/paths/instance/transfers/list/Table.tsx:59
+#, c-format
+msgid "Transfers"
+msgstr "Перекази"
+
+#: src/paths/instance/transfers/list/Table.tsx:64
+#, fuzzy, c-format
+msgid "Add new transfer"
+msgstr "додати новий переказ"
+
+#: src/paths/instance/transfers/list/Table.tsx:117
+#, fuzzy, c-format
+msgid "Load more transfers before the first one"
+msgstr "завантажити більше переказів до першого"
+
+#: src/paths/instance/transfers/list/Table.tsx:130
+#, c-format
+msgid "Credit"
+msgstr "Кредит"
+
+#: src/paths/instance/transfers/list/Table.tsx:133
+#, c-format
+msgid "Confirmed"
+msgstr "Підтверджено"
+
+#: src/paths/instance/transfers/list/Table.tsx:136
+#, c-format
+msgid "Verified"
+msgstr "Перевірено"
+
+#: src/paths/instance/transfers/list/Table.tsx:139
+#, c-format
+msgid "Executed at"
+msgstr "Виконано о"
+
+#: src/paths/instance/transfers/list/Table.tsx:150
+#, c-format
+msgid "yes"
+msgstr "так"
+
+#: src/paths/instance/transfers/list/Table.tsx:150
+#, c-format
+msgid "no"
+msgstr "ні"
+
+#: src/paths/instance/transfers/list/Table.tsx:155
+#, c-format
+msgid "never"
+msgstr "ніколи"
+
+#: src/paths/instance/transfers/list/Table.tsx:160
+#, c-format
+msgid "unknown"
+msgstr "невідомо"
+
+#: src/paths/instance/transfers/list/Table.tsx:166
+#, fuzzy, c-format
+msgid "Delete selected transfer from the database"
+msgstr "видалити вибраний переказ з бази даних"
+
+#: src/paths/instance/transfers/list/Table.tsx:181
+#, fuzzy, c-format
+msgid "Load more transfers after the last one"
+msgstr "завантажити більше переказів після останнього"
+
+#: src/paths/instance/transfers/list/Table.tsx:201
+#, c-format
+msgid "There is no transfer yet, add more pressing the + sign"
+msgstr "Переказів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:76
+#, c-format
+msgid "Bank account"
+msgstr "Банківський рахунок"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:83
+#, fuzzy, c-format
+msgid "All accounts"
+msgstr "Рахунок"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:84
+#, fuzzy, c-format
+msgid "Filter by account address"
+msgstr "фільтрувати за адресою рахунку"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:105
+#, fuzzy, c-format
+msgid "Only show wire transfers confirmed by the merchant"
+msgstr "показувати лише перекази, підтверджені продавцем"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:115
+#, fuzzy, c-format
+msgid "Only show wire transfers claimed by the exchange"
+msgstr "показувати лише перекази, заявлені обмінником"
+
+#: src/paths/instance/transfers/list/ListPage.tsx:118
+#, c-format
+msgid "Unverified"
+msgstr "Неперевірений"
+
+#: src/paths/instance/transfers/list/index.tsx:118
+#, fuzzy, c-format
+msgid "Wire transfer \"%1$s...\" has been deleted"
+msgstr "Інстанція \"%1$s\" (ID: %2$s) була видалена"
+
+#: src/paths/instance/transfers/list/index.tsx:123
+#, fuzzy, c-format
+msgid "Failed to delete transfer"
+msgstr "Не вдалося видалити інстанцію"
+
+#: src/paths/admin/create/CreatePage.tsx:86
+#, c-format
+msgid "Must be business or individual"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:104
+#, c-format
+msgid "Pay delay can't be greater than wire transfer delay"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:112
+#, fuzzy, c-format
+msgid "Max 7 lines"
+msgstr "максимум 7 рядків"
+
+#: src/paths/admin/create/CreatePage.tsx:138
+#, c-format
+msgid "Doesn't match"
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:215
+#, fuzzy, c-format
+msgid "Enable access control"
+msgstr "Управління токеном доступу"
+
+#: src/paths/admin/create/CreatePage.tsx:216
+#, c-format
+msgid "Choose if the backend server should authenticate access."
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:243
+#, c-format
+msgid "Access control is not yet decided. This instance can't be created."
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:250
+#, c-format
+msgid "Authorization must be handled externally."
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:256
+#, c-format
+msgid "Authorization is handled by the backend server."
+msgstr ""
+
+#: src/paths/admin/create/CreatePage.tsx:274
+#, c-format
+msgid "Need to complete marked fields and choose authorization method"
+msgstr "Необхідно заповнити позначені поля та вибрати метод авторизації"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:53
+#, c-format
+msgid ""
+"Name of the instance in URLs. The 'default' instance is special in that it "
+"is used to administer other instances."
+msgstr ""
+"Назва інстанції в URL. Інстанція 'default' є особливою, оскільки "
+"використовується для адміністрування інших інстанцій."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:59
+#, c-format
+msgid "Business name"
+msgstr "Назва бізнесу"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:60
+#, c-format
+msgid "Legal name of the business represented by this instance."
+msgstr "Юридична назва бізнесу, який представляє ця інстанція."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:67
+#, c-format
+msgid "Email"
+msgstr "Email"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:68
+#, c-format
+msgid "Contact email"
+msgstr "Контактний email"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:73
+#, c-format
+msgid "Website URL"
+msgstr "URL вебсайту"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:74
+#, c-format
+msgid "URL."
+msgstr "URL."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:79
+#, c-format
+msgid "Logo"
+msgstr "Логотип"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:80
+#, c-format
+msgid "Logo image."
+msgstr "Зображення логотипу."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:86
+#, c-format
+msgid "Physical location of the merchant."
+msgstr "Фізичне розташування продавця."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:93
+#, c-format
+msgid "Jurisdiction"
+msgstr "Юрисдикція"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:94
+#, c-format
+msgid "Jurisdiction for legal disputes with the merchant."
+msgstr "Юрисдикція для правових спорів з продавцем."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:101
+#, c-format
+msgid "Pay transaction fee"
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:102
+#, c-format
+msgid "Assume the cost of the transaction of let the user pay for it."
+msgstr ""
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:107
+#, c-format
+msgid "Default payment delay"
+msgstr "Затримка оплати за замовчуванням"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:109
+#, c-format
+msgid ""
+"Time customers have to pay an order before the offer expires by default."
+msgstr ""
+"Час, який мають клієнти для оплати замовлення до закінчення терміну дії "
+"пропозиції за замовчуванням."
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:114
+#, c-format
+msgid "Default wire transfer delay"
+msgstr "Затримка банківського переказу за замовчуванням"
+
+#: src/components/instance/DefaultInstanceFormFields.tsx:115
+#, 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:124
+#, c-format
+msgid "Instance id"
+msgstr "Ідентифікатор інстанції"
+
+#: src/paths/instance/update/index.tsx:108
+#, fuzzy, c-format
+msgid "Failed to update instance"
+msgstr "Не вдалося створити інстанцію"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:54
+#, c-format
+msgid "Must be \"pay\" or \"refund\""
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:59
+#, fuzzy, c-format
+msgid "Must be one of '%1$s'"
+msgstr "повинно бути одним із '%1$s'"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:85
+#, c-format
+msgid "Webhook ID to use"
+msgstr "ID вебхука для використання"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:89
+#, c-format
+msgid "Event"
+msgstr "Подія"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:91
+#, c-format
+msgid "Pay"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:95
+#, c-format
+msgid "The event of the webhook: why the webhook is used"
+msgstr "Подія вебхука: чому використовується вебхук"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:99
+#, c-format
+msgid "Method"
+msgstr "Метод"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:101
+#, c-format
+msgid "GET"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:102
+#, c-format
+msgid "POST"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:103
+#, c-format
+msgid "PUT"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:104
+#, c-format
+msgid "PATCH"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:105
+#, c-format
+msgid "HEAD"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:108
+#, c-format
+msgid "Method used by the webhook"
+msgstr "Метод, що використовується вебхуком"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:113
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:114
+#, c-format
+msgid "URL of the webhook where the customer will be redirected"
+msgstr "URL вебхука, куди буде перенаправлений клієнт"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:120
+#, c-format
+msgid ""
+"The text below support %1$s template engine. Any string between %2$s and "
+"%3$s will be replaced with replaced with the value of the corresponding "
+"variable."
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:138
+#, c-format
+msgid "For example %1$s will be replaced with the the order's price"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:145
+#, c-format
+msgid "The short list of variables are:"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:156
+#, fuzzy, c-format
+msgid "order's description"
+msgstr "опис"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:160
+#, fuzzy, c-format
+msgid "order's price"
+msgstr "Ціна замовлення"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:164
+#, c-format
+msgid "order's unique identification"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:172
+#, fuzzy, c-format
+msgid "the amount that was being refunded"
+msgstr "сума до повернення"
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:178
+#, c-format
+msgid "the reason entered by the merchant staff for granting the refund"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:185
+#, c-format
+msgid "time of the refund in nanoseconds since 1970"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:202
+#, c-format
+msgid "Http body"
+msgstr ""
+
+#: src/paths/instance/webhooks/create/CreatePage.tsx:203
+#, c-format
+msgid "Body template by the webhook"
+msgstr "Шаблон тіла вебхука"
+
+#: src/paths/instance/webhooks/create/index.tsx:52
+#, fuzzy, c-format
+msgid "Webhook create successfully"
+msgstr "вебхук успішно видалено"
+
+#: src/paths/instance/webhooks/create/index.tsx:58
+#, fuzzy, c-format
+msgid "Could not create the webhook"
+msgstr "не вдалося видалити вебхук"
+
+#: src/paths/instance/webhooks/create/index.tsx:66
+#, fuzzy, c-format
+msgid "Could not create webhook"
+msgstr "не вдалося видалити вебхук"
+
+#: src/paths/instance/webhooks/list/Table.tsx:57
+#, c-format
+msgid "Webhooks"
+msgstr "Вебхуки"
+
+#: src/paths/instance/webhooks/list/Table.tsx:62
+#, fuzzy, c-format
+msgid "Add new webhooks"
+msgstr "додати нові вебхуки"
+
+#: src/paths/instance/webhooks/list/Table.tsx:117
+#, fuzzy, c-format
+msgid "Load more webhooks before the first one"
+msgstr "завантажити більше вебхуків до першого"
+
+#: src/paths/instance/webhooks/list/Table.tsx:130
+#, c-format
+msgid "Event type"
+msgstr "Тип події"
+
+#: src/paths/instance/webhooks/list/Table.tsx:155
+#, fuzzy, c-format
+msgid "Delete selected webhook from the database"
+msgstr "видалити вибраний вебхук з бази даних"
+
+#: src/paths/instance/webhooks/list/Table.tsx:170
+#, fuzzy, c-format
+msgid "Load more webhooks after the last one"
+msgstr "завантажити більше вебхуків після останнього"
+
+#: src/paths/instance/webhooks/list/Table.tsx:190
+#, c-format
+msgid "There is no webhooks yet, add more pressing the + sign"
+msgstr "Вебхуків ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/webhooks/list/index.tsx:88
+#, fuzzy, c-format
+msgid "Webhook delete successfully"
+msgstr "вебхук успішно видалено"
+
+#: src/paths/instance/webhooks/list/index.tsx:93
+#, fuzzy, c-format
+msgid "Could not delete the webhook"
+msgstr "не вдалося видалити вебхук"
+
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:109
+#, c-format
+msgid "Header"
+msgstr "Заголовок"
+
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:111
+#, c-format
+msgid "Header template of the webhook"
+msgstr "Шаблон заголовка вебхука"
+
+#: src/paths/instance/webhooks/update/UpdatePage.tsx:116
+#, c-format
+msgid "Body"
+msgstr "Тіло"
+
+#: src/paths/instance/webhooks/update/index.tsx:88
+#, fuzzy, c-format
+msgid "Webhook updated"
+msgstr "ID вебхука для використання"
+
+#: src/paths/instance/webhooks/update/index.tsx:94
+#, fuzzy, c-format
+msgid "Could not update webhook"
+msgstr "не вдалося видалити вебхук"
+
+#: src/paths/settings/index.tsx:73
+#, c-format
+msgid "Language"
+msgstr ""
+
+#: src/paths/settings/index.tsx:96
+#, c-format
+msgid "Set default"
+msgstr ""
+
+#: src/paths/settings/index.tsx:102
+#, c-format
+msgid "Advance order creation"
+msgstr ""
+
+#: src/paths/settings/index.tsx:103
+#, c-format
+msgid "Shows more options in the order creation form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:107
+#, c-format
+msgid "Advance instance settings"
+msgstr ""
+
+#: src/paths/settings/index.tsx:108
+#, c-format
+msgid "Shows more options in the instance settings form"
+msgstr ""
+
+#: src/paths/settings/index.tsx:113
+#, fuzzy, c-format
+msgid "Date format"
+msgstr "недійсний формат"
+
+#: src/paths/settings/index.tsx:131
+#, c-format
+msgid "How the date is going to be displayed"
+msgstr ""
+
+#: src/paths/settings/index.tsx:134
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/paths/settings/index.tsx:135
+#, c-format
+msgid ""
+"Shows more options and tools which are not intended for general audience."
+msgstr ""
+
+#: src/paths/instance/categories/list/Table.tsx:133
+#, fuzzy, c-format
+msgid "Total products"
+msgstr "Загальна ціна"
+
+#: src/paths/instance/categories/list/Table.tsx:164
+#, fuzzy, c-format
+msgid "Delete selected category from the database"
+msgstr "видалити вибраний переказ з бази даних"
+
+#: src/paths/instance/categories/list/Table.tsx:199
+#, fuzzy, c-format
+msgid "There is no categories yet, add more pressing the + sign"
+msgstr "Шаблонів ще немає, додайте більше, натиснувши знак +"
+
+#: src/paths/instance/categories/list/index.tsx:90
+#, fuzzy, c-format
+msgid "Category delete successfully"
+msgstr "шаблон успішно видалено"
+
+#: src/paths/instance/categories/list/index.tsx:95
+#, fuzzy, c-format
+msgid "Could not delete the category"
+msgstr "не вдалося видалити шаблон"
+
+#: src/paths/instance/categories/create/CreatePage.tsx:77
+#, c-format
+msgid "Category name"
+msgstr ""
+
+#: src/paths/instance/categories/create/index.tsx:53
+#, fuzzy, c-format
+msgid "Category added successfully"
+msgstr "шаблон успішно видалено"
+
+#: src/paths/instance/categories/create/index.tsx:59
+#, fuzzy, c-format
+msgid "Could not add category"
+msgstr "не вдалося створити продукт"
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:102
+#, c-format
+msgid "Id:"
+msgstr ""
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:120
+#, fuzzy, c-format
+msgid "Name of the category"
+msgstr "Назва шаблону в URL."
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:124
+#, c-format
+msgid "Products"
+msgstr "Товари"
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:133
+#, fuzzy, c-format
+msgid "Search by product description or id"
+msgstr "шукати продукти за їхнім описом або ідентифікатором"
+
+#: src/paths/instance/categories/update/UpdatePage.tsx:134
+#, c-format
+msgid "Products that this category will list."
+msgstr ""
+
+#: src/paths/instance/categories/update/index.tsx:93
+#, fuzzy, c-format
+msgid "Could not update category"
+msgstr "не вдалося оновити шаблон"
+
+#: src/paths/instance/categories/update/index.tsx:95
+#, c-format
+msgid "Category id is unknown"
+msgstr ""
+
+#: src/Routing.tsx:659
+#, c-format
+msgid "Without this the merchant backend will refuse to create new orders."
+msgstr ""
+
+#: src/Routing.tsx:669
+#, c-format
+msgid "Hide for today"
+msgstr "Сховати на сьогодні"
+
+#: src/Routing.tsx:703
+#, fuzzy, c-format
+msgid "KYC verification needed"
+msgstr "Очікування перевірки KYC"
+
+#: src/Routing.tsx:707
+#, c-format
+msgid ""
+"Some transfer are on hold until a KYC process is completed. Go to the KYC "
+"section in the left panel for more information"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:157
+#, fuzzy, c-format
+msgid "Configuration"
+msgstr "Термін дії"
+
+#: src/components/menu/SideBar.tsx:196
+#, c-format
+msgid "Settings"
+msgstr "Налаштування"
+
+#: src/components/menu/SideBar.tsx:206
+#, fuzzy, c-format
+msgid "Access token"
+msgstr "Токен доступу"
+
+#: src/components/menu/SideBar.tsx:214
+#, c-format
+msgid "Connection"
+msgstr "З'єднання"
+
+#: src/components/menu/SideBar.tsx:223
+#, c-format
+msgid "Interface"
+msgstr ""
+
+#: src/components/menu/SideBar.tsx:264
+#, c-format
+msgid "List"
+msgstr "Список"
+
+#: src/components/menu/SideBar.tsx:283
+#, c-format
+msgid "Log out"
+msgstr "Вийти"
+
+#: src/paths/admin/create/index.tsx:54
+#, c-format
+msgid "Failed to create instance"
+msgstr "Не вдалося створити інстанцію"
+
+#: src/Application.tsx:208
+#, c-format
+msgid "checking compatibility with server..."
+msgstr ""
+
+#: src/Application.tsx:217
+#, fuzzy, c-format
+msgid "Contacting the server failed"
+msgstr "Не вдалося підключитися до сервера."
+
+#: src/Application.tsx:229
+#, c-format
+msgid "The server version is not supported"
+msgstr ""
+
+#: src/Application.tsx:230
+#, c-format
+msgid "Supported version \"%1$s\", server version \"%2$s\"."
+msgstr ""
+
+#: src/components/form/InputSecured.tsx:37
+#, c-format
+msgid "Deleting"
+msgstr "Видалення"
+
+#: src/components/form/InputSecured.tsx:41
+#, c-format
+msgid "Changing"
+msgstr "Зміна"
+
+#: src/components/form/InputSecured.tsx:88
+#, c-format
+msgid "Manage access token"
+msgstr "Управління токеном доступу"
+
+#: src/paths/admin/create/InstanceCreatedSuccessfully.tsx:52
+#, fuzzy, c-format
+msgid "Business Name"
+msgstr "Назва бізнесу"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:114
+#, c-format
+msgid "Order ID"
+msgstr "Ідентифікатор замовлення"
+
+#: src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx:128
+#, c-format
+msgid "Payment URL"
+msgstr "URL оплати"
+
+#, c-format
+#~ msgid "cannot be empty"
+#~ msgstr "не може бути порожнім"
+
+#, c-format
+#~ msgid "KYC URL"
+#~ msgstr "KYC URL"
+
+#, c-format
+#~ msgid "clear"
+#~ msgstr "очистити"
+
+#, c-format
+#~ msgid "Product"
+#~ msgstr "Продукт"
+
+#, c-format
+#~ msgid "image"
+#~ msgstr "зображення"
+
+#, c-format
+#~ msgid "quantity"
+#~ msgstr "кількість"
+
+#, c-format
+#~ msgid "total price"
+#~ msgstr "загальна ціна"
+
+#, c-format
+#~ msgid "not a valid json"
+#~ msgstr "недійсний json"
+
+#, c-format
+#~ msgid "Auto-refund deadline"
+#~ msgstr "Термін автоматичного повернення"
+
+#, c-format
+#~ msgid "Maximum deposit fee"
+#~ msgstr "Максимальна комісія за депозит"
+
+#, 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 ""
+#~ "Максимальна сукупна комісія за переказ, яку продавець готовий покрити для "
+#~ "цього замовлення. Комісії за переказ, що перевищують цю суму, повинні "
+#~ "бути покриті клієнтами."
+
+#, c-format
+#~ msgid "Wire fee amortization"
+#~ msgstr "Амортизація комісії за переказ"
+
+#, 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 ""
+#~ "Коефіцієнт, за яким комісії за переказ, що перевищують вищезазначений "
+#~ "поріг, діляться для визначення частки надлишкових комісій за переказ, яку "
+#~ "повинен сплатити споживач."
+
+#, c-format
+#~ msgid ""
+#~ "Uncheck this option if the merchant backend generated an order ID with "
+#~ "enough entropy to prevent adversarial claims."
+#~ msgstr ""
+#~ "Зніміть цю опцію, якщо бекенд продавця згенерував ідентифікатор "
+#~ "замовлення з достатньою ентропією для запобігання ворожих претензій."
+
+#, c-format
+#~ msgid "load newer orders"
+#~ msgstr "завантажити нові замовлення"
+
+#, c-format
+#~ msgid "load older orders"
+#~ msgstr "завантажити старіші замовлення"
+
+#, c-format
+#~ msgid "date"
+#~ msgstr "дата"
+
+#, c-format
+#~ msgid "amount"
+#~ msgstr "сума"
+
+#, c-format
+#~ msgid "reason"
+#~ msgstr "причина"
+
+#, c-format
+#~ msgid "Max wire fee"
+#~ msgstr "Максимальна комісія за переказ"
+
+#, c-format
+#~ msgid "maximum wire fee accepted by the merchant"
+#~ msgstr "максимальна комісія за переказ, прийнята продавцем"
+
+#, c-format
+#~ msgid ""
+#~ "over how many customer transactions does the merchant expect to amortize "
+#~ "wire fees on average"
+#~ msgstr ""
+#~ "на скільки транзакцій з клієнтами продавець очікує амортизувати комісії "
+#~ "за переказ в середньому"
+
+#, c-format
+#~ msgid "paid"
+#~ msgstr "оплачено"
+
+#, c-format
+#~ msgid "refunded"
+#~ msgstr "повернено"
+
+#, c-format
+#~ msgid "refund"
+#~ msgstr "повернення"
+
+#, c-format
+#~ msgid "created at"
+#~ msgstr "створено о"
+
+#, c-format
+#~ msgid "date (YYYY/MM/DD)"
+#~ msgstr "дата (РРРР/ММ/ДД)"
+
+#, c-format
+#~ msgid "could not get the order to refund"
+#~ msgstr "не вдалося отримати замовлення для повернення"
+
+#, c-format
+#~ msgid "Delivery address"
+#~ msgstr "Адреса доставки"
+
+#, c-format
+#~ msgid "Sell"
+#~ msgstr "Продати"
+
+#, c-format
+#~ msgid "Profit"
+#~ msgstr "Прибуток"
+
+#, c-format
+#~ msgid "free"
+#~ msgstr "безкоштовно"
+
+#, 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 ""
+#~ "Щоб завершити налаштування резерву, вам потрібно ініціювати банківський "
+#~ "переказ, використовуючи дане призначення переказу, і зарахувати зазначену "
+#~ "суму на вказаний рахунок обмінника."
+
+#, c-format
+#~ msgid ""
+#~ "If your system supports RFC 8905, you can do this by opening this URI:"
+#~ msgstr ""
+#~ "Якщо ваша система підтримує RFC 8905, ви можете зробити це, відкривши цей "
+#~ "URI:"
+
+#, c-format
+#~ msgid "it should be greater than 0"
+#~ msgstr "це повинно бути більше 0"
+
+#, c-format
+#~ msgid "must be a valid URL"
+#~ msgstr "повинен бути дійсний URL"
+
+#, c-format
+#~ msgid "Initial balance"
+#~ msgstr "Початковий баланс"
+
+#, c-format
+#~ msgid "balance prior to deposit"
+#~ msgstr "баланс до внесення депозиту"
+
+#, c-format
+#~ msgid "Next"
+#~ msgstr "Далі"
+
+#, c-format
+#~ msgid "method to use for wire transfer"
+#~ msgstr "метод для використання при банківському переказі"
+
+#, c-format
+#~ msgid "Select one wire method"
+#~ msgstr "Виберіть один метод переказу"
+
+#, c-format
+#~ msgid "Created balance"
+#~ msgstr "Створений баланс"
+
+#, c-format
+#~ msgid "Exchange balance"
+#~ msgstr "Баланс обмінника"
+
+#, c-format
+#~ msgid "Picked up"
+#~ msgstr "Отримано"
+
+#, c-format
+#~ msgid "Committed"
+#~ msgstr "Затверджено"
+
+#, c-format
+#~ msgid "Subject"
+#~ msgstr "Призначення"
+
+#, c-format
+#~ msgid "Tips"
+#~ msgstr "Чайові"
+
+#, c-format
+#~ msgid "No tips has been authorized from this reserve"
+#~ msgstr "З цього резерву не було авторизовано чайових"
+
+#, c-format
+#~ msgid "Authorized"
+#~ msgstr "Авторизовано"
+
+#, c-format
+#~ msgid "amount of tip"
+#~ msgstr "сума чайових"
+
+#, c-format
+#~ msgid "Justification"
+#~ msgstr "Обґрунтування"
+
+#, c-format
+#~ msgid "reason for the tip"
+#~ msgstr "причина для чайових"
+
+#, c-format
+#~ msgid "URL after tip"
+#~ msgstr "URL після чайових"
+
+#, c-format
+#~ msgid "URL to visit after tip payment"
+#~ msgstr "URL для відвідування після оплати чайових"
+
+#, c-format
+#~ msgid "Reserves not yet funded"
+#~ msgstr "Резерви ще не профінансовані"
+
+#, c-format
+#~ msgid "Reserves ready"
+#~ msgstr "Резерви готові"
+
+#, c-format
+#~ msgid "Expires at"
+#~ msgstr "Закінчується о"
+
+#, c-format
+#~ msgid "Initial"
+#~ msgstr "Початковий"
+
+#, c-format
+#~ msgid "authorize new tip from selected reserve"
+#~ msgstr "авторизувати нові чайові з вибраного резерву"
+
+#, c-format
+#~ msgid ""
+#~ "There is no ready reserves yet, add more pressing the + sign or fund them"
+#~ msgstr ""
+#~ "Готових резервів ще немає, додайте більше, натиснувши знак + або "
+#~ "профінансуйте їх"
+
+#, c-format
+#~ msgid "Expected Balance"
+#~ msgstr "Очікуваний баланс"
+
+#, c-format
+#~ msgid "should not be empty"
+#~ msgstr "не повинно бути порожнім"
+
+#, c-format
+#~ msgid "should be greater that 0"
+#~ msgstr "повинно бути більше 0"
+
+#, c-format
+#~ msgid "Fixed summary"
+#~ msgstr "Фіксований підсумок"
+
+#, c-format
+#~ msgid "Fixed price"
+#~ msgstr "Фіксована ціна"
+
+#, c-format
+#~ msgid "Point-of-sale key"
+#~ msgstr "Ключ точки продажу"
+
+#, c-format
+#~ msgid "Useful to validate the purchase"
+#~ msgstr "Корисний для підтвердження покупки"
+
+#, c-format
+#~ msgid "show secret key"
+#~ msgstr "показати секретний ключ"
+
+#, c-format
+#~ msgid "hide secret key"
+#~ msgstr "приховати секретний ключ"
+
+#, c-format
+#~ msgid "hide"
+#~ msgstr "приховати"
+
+#, c-format
+#~ msgid "show"
+#~ msgstr "показати"
+
+#, c-format
+#~ msgid "could not inform template"
+#~ msgstr "не вдалося сформувати шаблон"
+
+#, 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 ""
+#~ "Тут ви можете вказати значення за замовчуванням для полів, які не є "
+#~ "фіксованими. Значення за замовчуванням можуть бути відредаговані клієнтом "
+#~ "перед оплатою."
+
+#, c-format
+#~ msgid "Default summary"
+#~ msgstr "Підсумок за замовчуванням"
+
+#, c-format
+#~ msgid "Setup TOTP"
+#~ msgstr "Налаштування TOTP"
+
+#, c-format
+#~ msgid "load older templates"
+#~ msgstr "завантажити старіші шаблони"
+
+#, c-format
+#~ msgid "load newer webhooks"
+#~ msgstr "завантажити новіші вебхуки"
+
+#, c-format
+#~ msgid "load older webhooks"
+#~ msgstr "завантажити старіші вебхуки"
+
+#, c-format
+#~ msgid "load newer transfers"
+#~ msgstr "завантажити новіші перекази"
+
+#, c-format
+#~ msgid "load older transfers"
+#~ msgstr "завантажити старіші перекази"
+
+#, c-format
+#~ msgid "is not valid"
+#~ msgstr "недійсний"
+
+#, c-format
+#~ msgid "must be 1 or greater"
+#~ msgstr "має бути 1 або більше"
+
+#, c-format
+#~ msgid "change authorization configuration"
+#~ msgstr "змінити конфігурацію авторизації"
+
+#, c-format
+#~ msgid "Target type"
+#~ msgstr "Тип цілі"
+
+#, c-format
+#~ msgid "Bank account owner's name."
+#~ msgstr "Ім'я власника банківського рахунку."
+
+#, c-format
+#~ msgid "No accounts yet."
+#~ msgstr "Ще немає рахунків."
+
+#, c-format
+#~ msgid "Default max deposit fee"
+#~ msgstr "Максимальна комісія за депозит за замовчуванням"
+
+#, c-format
+#~ msgid ""
+#~ "Maximum deposit fees this merchant is willing to pay per order by default."
+#~ msgstr ""
+#~ "Максимальна комісія за депозит, яку цей продавець готовий платити за "
+#~ "замовлення за замовчуванням."
+
+#, c-format
+#~ msgid "Default max wire fee"
+#~ msgstr "Максимальна комісія за переказ за замовчуванням"
+
+#, c-format
+#~ msgid ""
+#~ "Maximum wire fees this merchant is willing to pay per wire transfer by "
+#~ "default."
+#~ msgstr ""
+#~ "Максимальна комісія за переказ, яку цей продавець готовий платити за "
+#~ "банківський переказ за замовчуванням."
+
+#, c-format
+#~ msgid "Default wire fee amortization"
+#~ msgstr "Амортизація комісії за переказ за замовчуванням"
+
+#, c-format
+#~ msgid ""
+#~ "Number of orders excess wire transfer fees will be divided by to compute "
+#~ "per order surcharge."
+#~ msgstr ""
+#~ "Кількість замовлень, на яку буде розподілена комісія за перевищення "
+#~ "банківських переказів, щоб обчислити додаткову плату за замовлення."
+
+#, c-format
+#~ msgid "Change the authorization method use for this instance."
+#~ msgstr "Змінити метод авторизації, що використовується для цієї інстанції."
+
+#, c-format
+#~ msgid "The request to the backend take too long and was cancelled"
+#~ msgstr "Запит до бекенду тривав занадто довго і був скасований"
+
+#, c-format
+#~ msgid "Diagnostic from %1$s is \"%2$s\""
+#~ msgstr "Діагностика від %1$s: \"%2$s\""
+
+#, c-format
+#~ msgid "The backend reported a problem: HTTP status #%1$s"
+#~ msgstr "Бекенд повідомив про проблему: HTTP статус #%1$s"
+
+#, c-format
+#~ msgid "Diagnostic from %1$s is '%2$s'"
+#~ msgstr "Діагностика від %1$s: '%2$s'"
+
+#, c-format
+#~ msgid "Access denied"
+#~ msgstr "Доступ заборонено"
+
+#, c-format
+#~ msgid "The access token provided is invalid."
+#~ msgstr "Наданий токен доступу є недійсним."
+
+#, c-format
+#~ msgid "The access token provided is invalid"
+#~ msgstr "Наданий токен доступу є недійсним"
+
+#, c-format
+#~ msgid "Instance"
+#~ msgstr "Інстанція"
+
+#, c-format
+#~ msgid "Check your token is valid"
+#~ msgstr "Перевірте, чи є ваш токен дійсним"
+
+#, c-format
+#~ msgid "Could not infer instance id from url %1$s"
+#~ msgstr "Не вдалося визначити ідентифікатор інстанції з URL %1$s"
+
+#, c-format
+#~ msgid "Server not found"
+#~ msgstr "Сервер не знайдено"
+
+#, c-format
+#~ msgid "Got message %1$s from %2$s"
+#~ msgstr "Отримано повідомлення %1$s від %2$s"
+
+#, c-format
+#~ msgid "Response from server is unreadable, http status: %1$s"
+#~ msgstr "Відповідь від сервера не читається, HTTP статус: %1$s"
+
+#, c-format
+#~ msgid "The value %1$s is invalid for a payment url"
+#~ msgstr "Значення %1$s є недійсним для URL оплати"
+
+#, c-format
+#~ msgid "add"
+#~ msgstr "додати"
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
index a28992a2f..406cfd698 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -25,25 +25,24 @@ import {
createRFC8959AccessTokenPlain,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+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 { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
+import { usePreference } from "../../../hooks/preference.js";
import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
+import { InputToggle } from "../../../components/form/InputToggle.js";
-export type Entity = Omit<
- Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">,
- "default_wire_transfer_delay"
-> & {
+export type Entity = TalerMerchantApi.InstanceConfigurationMessage & {
auth_token?: string;
- default_pay_delay: Duration;
- default_wire_transfer_delay: Duration;
+ // default_pay_delay: Duration;
+ // default_wire_transfer_delay: Duration;
};
interface Props {
@@ -52,140 +51,145 @@ interface Props {
forceId?: string;
}
+const twoHours = Duration.fromSpec({ hours: 2 });
+const twoDays = Duration.fromSpec({ days: 2 });
+
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
+ default_pay_delay: Duration.toTalerProtocolDuration(twoHours),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(twoDays),
};
}
-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);
+type TokenForm = { accessControl: boolean; token: string; repeat: string };
+export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
+ const [pref, updatePref] = usePreference();
const { i18n } = useTranslationContext();
+ const [value, valueHandler] = useState(with_defaults(forceId));
+ const [tokenForm, setTokenForm] = useState<Partial<TokenForm>>({});
- const errors: FormErrors<Entity> = {
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
id: !value.id
- ? i18n.str`required`
+ ? i18n.str`Required`
: !INSTANCE_ID_REGEX.test(value.id)
- ? i18n.str`is not valid`
+ ? i18n.str`Invalid`
: undefined,
- name: !value.name ? i18n.str`required` : undefined,
+ name: !value.name ? i18n.str`Required` : undefined,
user_type: !value.user_type
- ? i18n.str`required`
+ ? i18n.str`Required`
: value.user_type !== "business" && value.user_type !== "individual"
- ? i18n.str`should be business or individual`
+ ? i18n.str`Must be business or individual`
: undefined,
// accounts:
// !value.accounts || !value.accounts.length
- // ? i18n.str`required`
+ // ? i18n.str`Required`
// : undefinedIfEmpty(
// value.accounts.map((p) => {
// return !PAYTO_REGEX.test(p.payto_uri)
- // ? i18n.str`is not valid`
+ // ? i18n.str`Invalid`
// : 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`
+ ? i18n.str`Required`
+ : value.default_wire_transfer_delay !== undefined &&
+ value.default_wire_transfer_delay.d_us !== "forever" &&
+ value.default_pay_delay.d_us !== "forever" &&
+ value.default_pay_delay.d_us > value.default_wire_transfer_delay.d_us
+ ? 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`
+ ? i18n.str`Required`
: undefined,
address: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n.str`max 7 lines`
+ ? 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`
+ ? i18n.str`Max 7 lines`
: undefined,
}),
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as Record<string, unknown>)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
+
+ const tokenFormErrors = undefinedIfEmpty<FormErrors<TokenForm>>({
+ token:
+ tokenForm.accessControl === false
+ ? undefined
+ : !tokenForm.token
+ ? i18n.str`Required`
+ : undefined,
+ repeat:
+ tokenForm.accessControl === false
+ ? undefined
+ : !tokenForm.repeat
+ ? i18n.str`Required`
+ : tokenForm.repeat !== tokenForm.token
+ ? i18n.str`Doesn't match`
+ : undefined,
+ });
+
+ const hasTokenErrors = tokenFormErrors === undefined;
const submit = (): Promise<void> => {
// use conversion instead of this
const newValue = structuredClone(value);
- const newToken = newValue.auth_token;
+ const accessControl = !!tokenForm.accessControl;
newValue.auth_token = undefined;
- newValue.auth =
- newToken === null || newToken === undefined
- ? { method: "external" }
- : { method: "token", token: createRFC8959AccessTokenPlain(newToken) };
+ newValue.auth = !accessControl
+ ? { method: "external" }
+ : {
+ method: "token",
+ token: createRFC8959AccessTokenPlain(tokenForm.token!),
+ };
if (!newValue.address) newValue.address = {};
if (!newValue.jurisdiction) newValue.jurisdiction = {};
- // remove above use conversion
- // schema.validateSync(value, { abortEarly: false })
- newValue.default_pay_delay = Duration.toTalerProtocolDuration(
- newValue.default_pay_delay!,
- ) as any;
- newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration(
- newValue.default_wire_transfer_delay!,
- ) as any;
- // delete value.default_pay_delay;
- // delete value.default_wire_transfer_delay;
- return onCreate(
- newValue as any as TalerMerchantApi.InstanceConfigurationMessage,
- );
+ return onCreate(newValue as TalerMerchantApi.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);
+ <section class="section is-main-section">
+ <div class="tabs is-toggle is-fullwidth is-small">
+ <ul>
+ <li
+ class={!pref.advanceInstanceMode ? "is-active" : ""}
+ onClick={() => {
+ updatePref("advanceInstanceMode", false);
}}
- onConfirm={(newToken) => {
- updateToken(newToken);
- updateIsTokenDialogActive(false);
- updateIsTokenSet(true);
+ >
+ <a>
+ <span>
+ <i18n.Translate>Simple</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li
+ class={pref.advanceInstanceMode ? "is-active" : ""}
+ onClick={() => {
+ updatePref("advanceInstanceMode", true);
}}
- />
- )}
- </div>
- <div class="column" />
- </div>
-
- <section class="section is-main-section">
+ >
+ <a>
+ <span>
+ <i18n.Translate>Advanced</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>{" "}
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
@@ -194,53 +198,63 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
object={value}
valueHandler={valueHandler}
>
- <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} />
+ <DefaultInstanceFormFields
+ readonlyId={!!forceId}
+ showId={true}
+ showLessFields={!pref.advanceInstanceMode}
+ />
+ </FormProvider>
+ <FormProvider
+ errors={tokenFormErrors}
+ object={tokenForm}
+ valueHandler={setTokenForm}
+ >
+ <InputToggle<TokenForm>
+ name="accessControl"
+ threeState={tokenForm.accessControl === undefined}
+ label={i18n.str`Enable access control`}
+ help={i18n.str`Choose if the backend server should authenticate access.`}
+ />
+ <Input<TokenForm>
+ name="token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`Next access token to be used`}
+ readonly={
+ tokenForm.accessControl === false ||
+ tokenForm.accessControl === undefined
+ }
+ inputType="password"
+ />
+ <Input<TokenForm>
+ name="repeat"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`Confirm the same access token`}
+ readonly={
+ tokenForm.accessControl === false ||
+ tokenForm.accessControl === undefined
+ }
+ inputType="password"
+ />
</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 ? (
+ {tokenForm.accessControl === undefined ? (
<p class="is-size-6">
<i18n.Translate>
- Access token is not yet configured. This instance can't be
+ Access control is not yet decided. This instance can't be
created.
</i18n.Translate>
</p>
- ) : value.auth_token === undefined ? (
+ ) : !tokenForm.accessControl ? (
<p class="is-size-6">
<i18n.Translate>
- No access token. Authorization must be handled externally.
+ 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.
+ Authorization is handled by the backend server.
</i18n.Translate>
</p>
)}
@@ -254,11 +268,11 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
)}
<AsyncButton
onClick={submit}
- disabled={hasErrors || !isTokenSet}
+ disabled={hasErrors || !hasTokenErrors}
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields and choose authorization method`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
>
<i18n.Translate>Confirm</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
index 939f9b06a..0f88688c7 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx
@@ -20,6 +20,7 @@
import { h, VNode } from "preact";
import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js";
import { Entity } from "./index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function InstanceCreatedSuccessfully({
entity,
@@ -28,11 +29,14 @@ export function InstanceCreatedSuccessfully({
entity: Entity;
onConfirm: () => void;
}): VNode {
+ const { i18n } = useTranslationContext();
return (
<CreatedSuccessfully onConfirm={onConfirm}>
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label">ID</label>
+ <label class="label">
+ <i18n.Translate>ID</i18n.Translate>
+ </label>
</div>
<div class="field-body is-flex-grow-3">
<div class="field">
@@ -44,7 +48,9 @@ export function InstanceCreatedSuccessfully({
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label">Business Name</label>
+ <label class="label">
+ <i18n.Translate>Business Name</i18n.Translate>
+ </label>
</div>
<div class="field-body is-flex-grow-3">
<div class="field">
@@ -56,7 +62,9 @@ export function InstanceCreatedSuccessfully({
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label">Access token</label>
+ <label class="label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </label>
</div>
<div class="field-body is-flex-grow-3">
<div class="field">
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
index b00cfbe7d..abf635662 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
@@ -18,9 +18,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { TalerMerchantApi } from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../components/menu/index.js";
@@ -38,8 +36,7 @@ export type Entity = TalerMerchantApi.InstanceConfigurationMessage;
export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { lib } = useSessionContext();
- const { state, logIn } = useSessionContext();
+ const { lib, state, logIn } = useSessionContext();
return (
<Fragment>
@@ -48,12 +45,18 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
<CreatePage
onBack={onBack}
forceId={forceId}
- onCreate={async (
- d: TalerMerchantApi.InstanceConfigurationMessage,
- ) => {
+ onCreate={async (d: TalerMerchantApi.InstanceConfigurationMessage) => {
if (state.status !== "loggedIn") return;
try {
- await lib.instance.createInstance(state.token, d);
+ const resp = await lib.instance.createInstance(state.token, d);
+ if (resp.type === "fail") {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
if (d.auth.token) {
//if auth has been updated, request a new access token
const result = await lib.authenticate.createAccessTokenBearer(
@@ -72,16 +75,12 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
}
}
onConfirm();
- } catch (ex) {
- if (ex instanceof Error) {
- setNotif({
- message: i18n.str`Failed to create instance`,
- type: "ERROR",
- description: ex.message,
- });
- } else {
- console.error(ex);
- }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to create instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
}
}}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
index cff3c5a02..4e3d98aba 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -97,7 +97,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new instance`}
+ data-tooltip={i18n.str`Add new instance`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -151,8 +151,7 @@ function Table({
onPurge,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
- const { lib } = useSessionContext();
- const { impersonate } = useSessionContext();
+ const { lib, impersonate } = useSessionContext();
return (
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
index 940d14334..969a037a9 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx
@@ -51,8 +51,8 @@ export function View({
const showingInstances = showIsDeleted
? instances.filter((i) => i.deleted)
: showIsActive
- ? instances.filter((i) => !i.deleted)
- : instances;
+ ? instances.filter((i) => !i.deleted)
+ : instances;
return (
<section class="section is-main-section">
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
index 5b492e45c..f6c2f9e8f 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
@@ -19,10 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
@@ -41,31 +44,29 @@ interface Props {
instances: TalerMerchantApi.Instance[];
}
-export default function Instances({
- onCreate,
- onUpdate,
-}: Props): VNode {
+export default function Instances({ onCreate, onUpdate }: Props): VNode {
const result = useBackendInstances();
- const [deleting, setDeleting] =
- useState<TalerMerchantApi.Instance | null>(null);
- const [purging, setPurging] =
- useState<TalerMerchantApi.Instance | null>(null);
+ const [deleting, setDeleting] = useState<TalerMerchantApi.Instance | null>(
+ null,
+ );
+ const [purging, setPurging] = useState<TalerMerchantApi.Instance | null>(
+ null,
+ );
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
- if (!result) return <Loading />
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result.case)
+ assertUnreachable(result.case);
}
}
}
@@ -90,12 +91,22 @@ export default function Instances({
return;
}
try {
- await lib.instance.deleteInstance(state.token, deleting.id);
- // pushNotification({message: 'delete_success', type: 'SUCCESS' })
- setNotif({
- message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
- type: "SUCCESS",
- });
+ const resp = await lib.instance.deleteInstance(
+ state.token,
+ deleting.id,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Failed to delete instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
} catch (error) {
setNotif({
message: i18n.str`Failed to delete instance`,
@@ -117,11 +128,25 @@ export default function Instances({
return;
}
try {
- await lib.instance.deleteInstance(state.token, purging.id, { purge: true });
- setNotif({
- message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
- type: "SUCCESS",
- });
+ const resp = await lib.instance.deleteInstance(
+ state.token,
+ purging.id,
+ {
+ purge: true,
+ },
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been purge`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Failed to purge instance`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
} catch (error) {
setNotif({
message: i18n.str`Failed to purge instance`,
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
index d0e7a83cd..953e412fe 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -19,9 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ PaytoUri,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -31,11 +39,16 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
-import { ImportingAccountModal } from "../../../../components/modal/index.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import {
+ CompareAccountsModal,
+ ImportingAccountModal,
+} from "../../../../components/modal/index.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { safeConvertURL } from "../update/UpdatePage.js";
+import { testRevenueAPI } from "./index.js";
-type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string };
+type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean };
interface Props {
onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>;
@@ -50,8 +63,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const [importing, setImporting] = useState(false);
const [state, setState] = useState<Partial<Entity>>({});
const facadeURL = safeConvertURL(state.credit_facade_url);
+
+ const [revenuePayto, setRevenuePayto] = useState<PaytoUri | undefined>(
+ // parsePaytoUri("payto://x-taler-bank/asd.com:1010/asd/pepe"),
+ undefined,
+ );
+ const [testError, setTestError] = useState<TranslatedString | undefined>(
+ undefined,
+ );
const errors: FormErrors<Entity> = {
- payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined,
credit_facade_credentials: !state.credit_facade_credentials
? undefined
@@ -59,12 +80,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
username:
state.credit_facade_credentials.type === "basic" &&
!state.credit_facade_credentials.username
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
password:
state.credit_facade_credentials.type === "basic" &&
!state.credit_facade_credentials.password
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
}),
credit_facade_url: !state.credit_facade_url
@@ -72,19 +93,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: !facadeURL
? i18n.str`Invalid url`
: !facadeURL.href.endsWith("/")
- ? i18n.str`URL should end with a '/'`
+ ? i18n.str`URL must end with a '/'`
: facadeURL.searchParams.size > 0
- ? i18n.str`URL should not contain params`
+ ? i18n.str`URL must not contain params`
: facadeURL.hash
- ? i18n.str`URL should not hash param`
+ ? i18n.str`URL must not hash param`
: 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(
@@ -117,25 +131,73 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
credit_facade_url,
});
};
- return (
- <div>
- {importing && <ImportingAccountModal onCancel={()=> {setImporting(false)}} onConfirm={(ac) => {
- state.payto_uri = ac.accountURI
- const u = new URL(ac.infoURL)
- u.password = ""
- if (u.username || u.password) {
- state.credit_facade_credentials = {
- type: "basic",
- password: u.password,
- username: u.username,
+
+ async function testAccountInfo() {
+ const revenueAPI = !state.credit_facade_url
+ ? undefined
+ : new URL("./", state.credit_facade_url);
+
+ if (revenueAPI) {
+ const resp = await testRevenueAPI(
+ revenueAPI,
+ state.credit_facade_credentials,
+ );
+ if (resp instanceof TalerError) {
+ setTestError(i18n.str`The request to check the revenue API failed.`);
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ } else if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.BadRequest: {
+ setTestError(i18n.str`Server replied with "bad request".`);
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized: {
+ setTestError(i18n.str`Unauthorized, check credentials.`);
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setTestError(
+ i18n.str`The endpoint doesn't seems to be a Taler Revenue API.`,
+ );
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
}
- state.repeatPassword = u.password
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ } else {
+ const found = resp.body;
+ const match = state.payto_uri === found;
+ setState({
+ ...state,
+ verified: match,
+ });
+ if (!match) {
+ setRevenuePayto(parsePaytoUri(resp.body));
}
- u.password = ""
- u.username = ""
- state.credit_facade_url = u.href;
- setImporting(false)
- }} />}
+ setTestError(undefined);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -147,12 +209,20 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputPaytoForm<Entity>
name="payto_uri"
- label={i18n.str`Account`}
+ label={i18n.str`Account details`}
/>
+ <div class="message-body" style={{ marginBottom: 10 }}>
+ <p>
+ <i18n.Translate>
+ If the bank supports Taler Revenue API then you can add the
+ endpoint URL below to keep the revenue information in sync.
+ </i18n.Translate>
+ </p>
+ </div>
<Input<Entity>
name="credit_facade_url"
- label={i18n.str`Account info URL`}
- help="https://bank.com"
+ label={i18n.str`Endpoint URL`}
+ help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/"
expand
tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
/>
@@ -179,21 +249,43 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
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}
+ <InputToggle<Entity>
+ label={i18n.str`Match`}
+ tooltip={i18n.str`Check where the information match against the server info.`}
+ name="verified"
+ readonly
+ threeState
+ help={
+ testError !== undefined
+ ? testError
+ : state.verified === undefined
+ ? i18n.str`Not verified`
+ : state.verified
+ ? i18n.str`Last test was ok`
+ : i18n.str`Last test failed`
+ }
+ side={
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`Compare info from server with account form`}
+ disabled={!state.credit_facade_url}
+ onClick={async () => {
+ await testAccountInfo();
+ }}
+ >
+ <i18n.Translate>Test</i18n.Translate>
+ </button>
+ }
+ />
</FormProvider>
<div class="buttons is-right mt-5">
<button
class="button is-info"
- data-tooltip={i18n.str`Need to complete marked fields`}
onClick={() => {
- setImporting(true)
+ setImporting(true);
}}
>
<i18n.Translate>Import from bank</i18n.Translate>
@@ -209,7 +301,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
@@ -220,6 +312,52 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
<div class="column" />
</div>
</section>
- </div>
+ {!importing ? undefined : (
+ <ImportingAccountModal
+ onCancel={() => {
+ setImporting(false);
+ }}
+ onConfirm={(ac) => {
+ const u = new URL(ac.infoURL);
+ const user = u.username;
+ const pwd = u.password;
+ u.password = "";
+ u.username = "";
+ const credit_facade_url = u.href;
+ setState({
+ payto_uri: ac.accountURI,
+ credit_facade_credentials:
+ user || pwd
+ ? {
+ type: "basic",
+ password: pwd,
+ username: user,
+ }
+ : undefined,
+ credit_facade_url,
+ });
+ setImporting(false);
+ }}
+ />
+ )}
+ {!revenuePayto ? undefined : (
+ <CompareAccountsModal
+ onCancel={() => {
+ setRevenuePayto(undefined);
+ }}
+ onConfirm={(d) => {
+ setState({
+ ...state,
+ payto_uri: d,
+ });
+ setRevenuePayto(undefined);
+ }}
+ formPayto={
+ !state.payto_uri ? undefined : parsePaytoUri(state.payto_uri)
+ }
+ testPayto={revenuePayto}
+ />
+ )}
+ </Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
index aa1481a2e..9e3b1b4bc 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -28,12 +28,11 @@ import {
TalerError,
TalerMerchantApi,
TalerRevenueHttpClient,
- assertUnreachable,
- opFixedSuccess,
+ opFixedSuccess
} from "@gnu-taler/taler-util";
import {
BrowserFetchHttpLib,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -49,9 +48,9 @@ interface Props {
}
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { lib: api } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ // const [tested, setTested] = useState(false);
const { i18n } = useTranslationContext();
return (
@@ -60,78 +59,24 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={async (request: Entity) => {
- const revenueAPI = !request.credit_facade_url
- ? undefined
- : new URL("./", request.credit_facade_url);
-
- if (revenueAPI) {
- const resp = await testRevenueAPI(
- revenueAPI,
- request.credit_facade_credentials,
- request.payto_uri,
- );
- if (resp instanceof TalerError) {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`The request to check the revenue API failed.`,
- details: JSON.stringify(resp.errorDetail, undefined, 2),
- });
- return;
- }
- if (resp.type === "fail") {
- switch (resp.case) {
- case HttpStatusCode.BadRequest: {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`Server replied with "bad request".`,
- });
- return;
-
- }
- case HttpStatusCode.Unauthorized: {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`Unauthorized, try with another credentials.`,
- });
- return;
-
- }
- case HttpStatusCode.NotFound: {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.ANOTHER_ACCOUNT: {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
- });
- return;
- }
- default: {
- assertUnreachable(resp);
- }
- }
- }
- }
-
- return api.instance
+ return lib.instance
.addBankAccount(state.token, request)
- .then(() => {
+ .then((resp) => {
+ if (resp.type === "fail") {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
onConfirm();
})
.catch((error) => {
setNotif({
- message: i18n.str`could not create account`,
+ message: i18n.str`Could not create account`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
@@ -147,12 +92,13 @@ export enum TestRevenueErrorType {
export async function testRevenueAPI(
revenueAPI: URL,
creds: FacadeCredentials | undefined,
- account: PaytoString,
-): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>
-| OperationFail<HttpStatusCode.Unauthorized>
-| OperationFail<HttpStatusCode.BadRequest>
-| OperationFail<TestRevenueErrorType.ANOTHER_ACCOUNT>
-| TalerError> {
+): Promise<
+ | OperationOk<PaytoString>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | TalerError
+> {
const api = new TalerRevenueHttpClient(
revenueAPI.href,
new BrowserFetchHttpLib(),
@@ -176,21 +122,12 @@ export async function testRevenueAPI(
return config;
}
- const history = await api.getHistory(auth);
-
- if (history.type === "fail") {
- return history;
- }
- if (history.body.credit_account !== account) {
- return {
- type: "fail",
- case: TestRevenueErrorType.ANOTHER_ACCOUNT,
- detail: {
- code: 1,
- hint: history.body.credit_account
- },
- };
+ const resp = await api.getHistory(auth);
+ if (resp.type === "fail") {
+ return resp;
}
+
+ return opFixedSuccess(resp.body.credit_account as PaytoString);
} catch (err) {
if (err instanceof TalerError) {
return err;
@@ -200,7 +137,6 @@ export async function testRevenueAPI(
// detail: err.errorDetail,
// };
}
+ throw err;
}
-
- return opFixedSuccess(undefined);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
deleted file mode 100644
index 4ee68cd80..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { CardTable } from "./Table.js";
-
-export interface Props {
- devices: TalerMerchantApi.BankAccountSummaryEntry[];
- // onLoadMoreBefore?: () => void;
- // onLoadMoreAfter?: () => void;
- onCreate: () => void;
- onDelete: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
- onSelect: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
-}
-
-export function ListPage({
- devices,
- onCreate,
- onDelete,
- onSelect,
- // onLoadMoreBefore,
- // onLoadMoreAfter,
-}: Props): VNode {
-
- 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/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
index a9cb2805b..8ab782cf4 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -19,12 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ parsePaytoUri,
+ PaytoType,
+ PaytoUri,
+ PaytoUriBitcoin,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ PaytoUriUnknown,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { StateUpdater, useState } from "preact/hooks";
-type Entity = TalerMerchantApi.BankAccountSummaryEntry;
+type Entity = TalerMerchantApi.BankAccountEntry;
interface Props {
accounts: Entity[];
@@ -55,7 +64,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new accounts`}
+ data-tooltip={i18n.str`Add new account`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -93,241 +102,255 @@ interface TableProps {
rowSelectionHandler: StateUpdater<string[]>;
}
-function Table({
- accounts,
- onDelete,
- onSelect,
-}: TableProps): VNode {
+function Table({ accounts, onDelete, onSelect }: TableProps): VNode {
const { i18n } = useTranslationContext();
- const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
+ 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 })
+ 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 })
+ 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 prev;
+ }, emptyList);
+ const bitcoinAccounts = accountsByType["bitcoin"];
+ const talerbankAccounts = accountsByType["x-taler-bank"];
+ const ibanAccounts = accountsByType["iban"];
+ const unknownAccounts = accountsByType["unknown"];
return (
<Fragment>
+ {bitcoinAccounts.length > 0 && (
+ <div class="table-container">
+ <p class="card-header-title">
+ <i18n.Translate>Wire method: Bitcoin</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)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ )}
- {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>}
+ {talerbankAccounts.length > 0 && (
+ <div class="table-container">
+ <p class="card-header-title">
+ <i18n.Translate>Wire method: x-taler-bank</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)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </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 />
- </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 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>Wire method: IBAN</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 />
+ </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 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)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </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>}
+ {unknownAccounts.length > 0 && (
+ <div class="table-container">
+ <p class="card-header-title">
+ <i18n.Translate>Other 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>
+ {unknownAccounts.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)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ )}
</Fragment>
-
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
index 1eda7382d..f68562ad6 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -19,10 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -33,37 +36,33 @@ import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-import { ListPage } from "./ListPage.js";
+import { CardTable } from "./Table.js";
interface Props {
onCreate: () => void;
onSelect: (id: string) => void;
}
-export default function ListOtpDevices({
- onCreate,
- onSelect,
-}: Props): VNode {
+export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib: api } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useInstanceBankAccounts();
- if (!result) return <Loading />
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.NotFound: {
- return <NotFoundPageOrAdminCreate />
+ return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -71,41 +70,52 @@ export default function ListOtpDevices({
return (
<Fragment>
<NotificationCard notification={notif} />
- {result.body.accounts.length < 1 &&
- <NotificationCard notification={{
- type: "WARN",
- message: i18n.str`You need to associate a bank account to receive revenue.`,
- description: i18n.str`Without this the merchant backend will refuse to create new orders.`
- }} />
- }
- <ListPage
- devices={result.body.accounts}
- // onLoadMoreBefore={
- // result.isFirstPage ? undefined: result.loadFirst
- // }
- // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
- onCreate={onCreate}
- onSelect={(e) => {
- onSelect(e.h_wire);
- }}
- onDelete={(e: TalerMerchantApi.BankAccountSummaryEntry) => {
- return api.instance.deleteBankAccount(state.token, 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,
- }),
- )
- }
- }
- />
+ {result.body.accounts.length < 1 && (
+ <NotificationCard
+ notification={{
+ type: "WARN",
+ message: i18n.str`You need to associate a bank account to receive revenue.`,
+ description: i18n.str`Without this the you won't be able to create new orders.`,
+ }}
+ />
+ )}
+ <section class="section is-main-section">
+ <CardTable
+ accounts={result.body.accounts.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={async (e: TalerMerchantApi.BankAccountEntry) => {
+ return lib.instance
+ .deleteBankAccount(state.token, e.h_wire)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Bank account delete successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not delete the bank account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`Could not delete the bank account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ }),
+ );
+ }}
+ />
+ </section>
</Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
index 1a8e9bdc1..e6dcfec7b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -19,9 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ PaytoString,
+ PaytoUri,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -31,73 +40,101 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { WithId } from "../../../../declaration.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { testRevenueAPI } from "../create/index.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import {
+ CompareAccountsModal,
+ ImportingAccountModal,
+} from "../../../../components/modal/index.js";
-type Entity = TalerMerchantApi.BankAccountEntry & WithId;
-
+type Entity = TalerMerchantApi.BankAccountDetail & WithId;
+type FormType = TalerMerchantApi.AccountPatchDetails & {
+ verified: boolean;
+ payto_uri?: PaytoString;
+};
const accountAuthType = ["unedit", "none", "basic"];
interface Props {
onUpdate: (d: TalerMerchantApi.AccountPatchDetails) => Promise<void>;
+ onReplace: (
+ prev: TalerMerchantApi.BankAccountDetail,
+ next: TalerMerchantApi.AccountAddDetails,
+ ) => Promise<void>;
onBack?: () => void;
account: Entity;
}
-export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+export function UpdatePage({
+ account,
+ onUpdate,
+ onBack,
+ onReplace,
+}: Props): VNode {
const { i18n } = useTranslationContext();
- const [state, setState] =
- useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account);
+ const [state, setState] = useState<Partial<FormType>>({
+ payto_uri: account.payto_uri,
+ credit_facade_url: account.credit_facade_url,
+ credit_facade_credentials: {
+ // @ts-expect-error unofficial unedited value
+ type: "unedit",
+ },
+ });
+ const [importing, setImporting] = useState(false);
- // @ts-expect-error "unedit" is fine since is part of the accountAuthType values
- if (state.credit_facade_credentials?.type === "unedit") {
- // we use this to set creds to undefined but server don't get this type
- state.credit_facade_credentials = undefined;
- }
+ const [revenuePayto, setRevenuePayto] = useState<PaytoUri | undefined>(
+ // parsePaytoUri("payto://x-taler-bank/asd.com:1010/asd/pepe"),
+ undefined,
+ );
+ const [testError, setTestError] = useState<TranslatedString | undefined>(
+ undefined,
+ );
+
+ const replacingAccountId = state.payto_uri !== account.payto_uri;
const facadeURL = safeConvertURL(state.credit_facade_url);
- const errors: FormErrors<TalerMerchantApi.AccountPatchDetails> = {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined,
+
credit_facade_url: !state.credit_facade_url
? undefined
: !facadeURL
? i18n.str`Invalid url`
: !facadeURL.href.endsWith("/")
- ? i18n.str`URL should end with a '/'`
+ ? i18n.str`URL must end with a '/'`
: facadeURL.searchParams.size > 0
- ? i18n.str`URL should not contain params`
+ ? i18n.str`URL must not contain params`
: facadeURL.hash
- ? i18n.str`URL should not hash param`
+ ? i18n.str`URL must not hash param`
+ : undefined,
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ type:
+ replacingAccountId &&
+ // @ts-expect-error unedit is not in facade creds
+ state.credit_facade_credentials?.type === "unedit"
+ ? i18n.str`Required`
+ : undefined,
+ username:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.username
+ ? i18n.str`Required`
: 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`doesn't match`
- : undefined,
- }),
- };
+ password:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.password
+ ? i18n.str`Required`
+ : undefined,
+ }),
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const submitForm = () => {
if (hasErrors) return Promise.reject();
@@ -111,21 +148,96 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
credit_facade_url == undefined ||
state.credit_facade_credentials === undefined
? undefined
- : state.credit_facade_credentials.type === "basic"
- ? {
- type: "basic",
- password: state.credit_facade_credentials.password,
- username: state.credit_facade_credentials.username,
- }
- : {
- type: "none",
- };
-
- return onUpdate({ credit_facade_credentials, credit_facade_url });
+ : // @ts-expect-error unedit is not in facade creds
+ state.credit_facade_credentials.type === "unedit"
+ ? undefined
+ : state.credit_facade_credentials.type === "basic"
+ ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
+ : {
+ type: "none",
+ };
+
+ if (replacingAccountId) {
+ return onReplace(account, {
+ payto_uri: state.payto_uri!,
+ credit_facade_credentials,
+ credit_facade_url,
+ });
+ } else {
+ return onUpdate({ credit_facade_credentials, credit_facade_url });
+ }
};
+ async function testAccountInfo() {
+ const revenueAPI = !state.credit_facade_url
+ ? undefined
+ : new URL("./", state.credit_facade_url);
+
+ if (revenueAPI) {
+ const resp = await testRevenueAPI(
+ revenueAPI,
+ state.credit_facade_credentials,
+ );
+ if (resp instanceof TalerError) {
+ setTestError(i18n.str`The request to check the revenue API failed.`);
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ } else if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.BadRequest: {
+ setTestError(i18n.str`Server replied with "bad request".`);
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized: {
+ setTestError(i18n.str`Unauthorized, check credentials.`);
+ setState({
+ ...state,
+ verified: false,
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setTestError(
+ i18n.str`The endpoint doesn't seems to be a Taler Revenue API.`,
+ );
+ setState({
+ ...state,
+ verified: undefined,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ } else {
+ const found = resp.body;
+ const match = state.payto_uri === found;
+ setState({
+ ...state,
+ verified: match,
+ });
+ if (!match) {
+ setRevenuePayto(parsePaytoUri(resp.body));
+ }
+ setTestError(undefined);
+ }
+ }
+ }
+
return (
- <div>
+ <Fragment>
<section class="section">
<section class="hero is-hero-bar">
<div class="hero-body">
@@ -133,7 +245,8 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- Account: <b>{account.id.substring(0, 8)}...</b>
+ <i18n.Translate>Account:</i18n.Translate>{" "}
+ <b>{account.id.substring(0, 8)}...</b>
</span>
</div>
</div>
@@ -150,15 +263,23 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
valueHandler={setState}
errors={errors}
>
- <InputPaytoForm<Entity>
+ <InputPaytoForm<FormType>
name="payto_uri"
label={i18n.str`Account`}
- readonly
/>
+ <div class="message-body" style={{ marginBottom: 10 }}>
+ <p>
+ <i18n.Translate>
+ If the bank supports Taler Revenue API then you can add
+ the endpoint URL below to keep the revenue information in
+ sync.
+ </i18n.Translate>
+ </p>
+ </div>
<Input<Entity>
name="credit_facade_url"
- label={i18n.str`Account info URL`}
- help="https://bank.com"
+ label={i18n.str`Endpoint URL`}
+ help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/"
expand
tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
/>
@@ -168,9 +289,9 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
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";
+ if (str === "none") return i18n.str`Without authentication`;
+ if (str === "basic") return i18n.str`With authentication`;
+ return i18n.str`Do not change`;
}}
/>
{state.credit_facade_credentials?.type === "basic" ? (
@@ -186,13 +307,36 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
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}
+ <InputToggle<FormType>
+ label={i18n.str`Match`}
+ tooltip={i18n.str`Check where the information match against the server info.`}
+ name="verified"
+ readonly
+ threeState
+ help={
+ testError !== undefined
+ ? testError
+ : state.verified === undefined
+ ? i18n.str`Not verified`
+ : state.verified
+ ? i18n.str`Last test was ok`
+ : i18n.str`Last test failed`
+ }
+ side={
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`Compare info from server with account form`}
+ disabled={!state.credit_facade_url}
+ onClick={async () => {
+ await testAccountInfo();
+ }}
+ >
+ <i18n.Translate>Test</i18n.Translate>
+ </button>
+ }
+ />
</FormProvider>
<div class="buttons is-right mt-5">
@@ -206,7 +350,7 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
@@ -217,7 +361,53 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
</div>
</section>
</section>
- </div>
+ {!importing ? undefined : (
+ <ImportingAccountModal
+ onCancel={() => {
+ setImporting(false);
+ }}
+ onConfirm={(ac) => {
+ const u = new URL(ac.infoURL);
+ const user = u.username;
+ const pwd = u.password;
+ u.password = "";
+ u.username = "";
+ const credit_facade_url = u.href;
+ setState({
+ payto_uri: ac.accountURI,
+ credit_facade_credentials:
+ user || pwd
+ ? {
+ type: "basic",
+ password: pwd,
+ username: user,
+ }
+ : undefined,
+ credit_facade_url,
+ });
+ setImporting(false);
+ }}
+ />
+ )}
+ {!revenuePayto ? undefined : (
+ <CompareAccountsModal
+ onCancel={() => {
+ setRevenuePayto(undefined);
+ }}
+ onConfirm={(d) => {
+ setState({
+ ...state,
+ payto_uri: d,
+ });
+ setRevenuePayto(undefined);
+ }}
+ formPayto={
+ !state.payto_uri ? undefined : parsePaytoUri(state.payto_uri)
+ }
+ testPayto={revenuePayto}
+ />
+ )}
+ </Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
index 9116aaa62..fc031625a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -19,10 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -33,8 +36,8 @@ import { useBankAccountDetails } from "../../../../hooks/bank.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js";
import { UpdatePage } from "./UpdatePage.js";
+import { WithId } from "../../../../declaration.js";
export type Entity = TalerMerchantApi.AccountPatchDetails & WithId;
@@ -48,8 +51,7 @@ export default function UpdateValidator({
onConfirm,
onBack,
}: Props): VNode {
- const { lib: api } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useBankAccountDetails(bid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
@@ -65,7 +67,7 @@ export default function UpdateValidator({
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -80,76 +82,71 @@ export default function UpdateValidator({
account={{ ...result.body, id: bid }}
onBack={onBack}
onUpdate={async (request) => {
- const revenueAPI = !request.credit_facade_url
- ? undefined
- : new URL("./", request.credit_facade_url);
-
- if (revenueAPI) {
- const resp = await testRevenueAPI(
- revenueAPI,
- request.credit_facade_credentials,
- result.body.payto_uri,
+ return lib.instance
+ .updateBankAccount(state.token, bid, request)
+ .then((resp) => {
+ if (resp.type === "fail") {
+ setNotif({
+ message: i18n.str`Could not update account`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ onConfirm();
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`Could not update account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
+ });
+ }}
+ onReplace={async (prev, next) => {
+ try {
+ const resp = await lib.instance.addBankAccount(
+ state.token,
+ next,
);
- if (resp instanceof TalerError) {
+ if (resp.type === "fail") {
setNotif({
message: i18n.str`Could not create account`,
type: "ERROR",
- description: i18n.str`The request to check the revenue API failed.`,
- details: JSON.stringify(resp.errorDetail, undefined, 2),
+ description: resp.detail?.hint,
});
return;
}
- if (resp.type === "fail") {
- switch (resp.case) {
- case HttpStatusCode.BadRequest: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`Server replied with "bad request".`,
- });
- return;
-
- }
- case HttpStatusCode.Unauthorized: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`Unauthorized, try with another credentials.`,
- });
- return;
-
- }
- case HttpStatusCode.NotFound: {
- setNotif({
- message: i18n.str`Could not create account`,
- type: "ERROR",
- description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
- });
- return;
- }
- case TestRevenueErrorType.ANOTHER_ACCOUNT: {
- setNotif({
- message: i18n.str`Could not add bank account`,
- type: "ERROR",
- description: i18n.str`The account info URL returned information from an account which is not the same in the account form: ${resp.detail.hint}`,
- });
- return;
- }
- default: {
- assertUnreachable(resp);
- }
- }
- }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
+ return;
}
- return api.instance.updateBankAccount(state.token, bid, request)
- .then(onConfirm)
- .catch((error) => {
+ try {
+ const resp = await lib.instance.deleteBankAccount(
+ state.token,
+ prev.h_wire,
+ );
+ if (resp.type === "fail") {
setNotif({
- message: i18n.str`could not update account`,
+ message: i18n.str`Could not delete account`,
type: "ERROR",
- description: error.message,
+ description: resp.detail?.hint,
});
+ return;
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not delete account`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
});
+ return;
+ }
+ onConfirm();
}}
/>
</Fragment>
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/Create.stories.tsx
index 26f851cc8..36b31ebe8 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx
index becaf8f3a..af8cf1c0b 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -19,16 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import {
+ TalerMerchantApi
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
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";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
-type Entity = MerchantBackend.Products.ProductAddDetail & {
- product_id: string;
-};
+type Entity = TalerMerchantApi.CategoryCreateRequest;
interface Props {
onCreate: (d: Entity) => Promise<void>;
@@ -36,22 +41,42 @@ interface Props {
}
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();
+ const [state, setState] = useState<Partial<Entity>>({name_i18n: {}});
+
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
+ name: !state.name
+ ? i18n.str`Required`
+ : !/[a-zA-Z0-9]*/.test(state.name)
+ ? i18n.str`Invalid. Only characters and numbers`
+ : undefined,
+ });
+
+ const hasErrors = errors !== undefined;
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as Entity);
+ };
+
return (
<div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
- <ProductForm onSubscribe={addFormSubmitter} />
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`Category name`}
+ />{" "}
+ </FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
@@ -60,13 +85,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
</button>
)}
<AsyncButton
- onClick={submitForm}
+ disabled={hasErrors}
data-tooltip={
- !submitForm
+ hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
- disabled={!submitForm}
+ onClick={submitForm}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx
index 25551a031..0a8c264f1 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -19,46 +19,55 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useInstanceDetails } from "../../../../hooks/instance.js";
-import { useTransferAPI } from "../../../../hooks/transfer.js";
+import { useSessionContext } from "../../../../context/session.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;
+type Entity = TalerMerchantApi.CategoryCreateRequest;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
-export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { informTransfer } = useTransferAPI();
+export default function CreateCategory({ onConfirm, onBack }: Props): VNode {
+ const { state, lib } = useSessionContext();
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())
+ onCreate={async (request: Entity) => {
+ return lib.instance
+ .addCategory(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Category added successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm()
+ } else {
+ setNotif({
+ message: i18n.str`Could not add category`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not inform transfer`,
+ message: i18n.str`Could not add category`,
type: "ERROR",
- description: error.message,
+ description:
+ error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx
index 0c28027fe..876d035df 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -19,12 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
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;
+type Entity = TalerMerchantApi.CategoryListEntry;
interface Props {
devices: Entity[];
@@ -32,8 +32,6 @@ interface Props {
onSelect: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -44,8 +42,6 @@ export function CardTable({
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -56,14 +52,14 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-newspaper" />
+ <i class="mdi mdi-label" />
</span>
- <i18n.Translate>OTP Devices</i18n.Translate>
+ <i18n.Translate>Categories</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new devices`}
+ data-tooltip={i18n.str`Add new devices`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -85,8 +81,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -104,35 +98,26 @@ interface TableProps {
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 && (
+ {onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices before the first one`}
+ data-tooltip={i18n.str`Load more devices before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load newer devices</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -142,7 +127,10 @@ function Table({
<i18n.Translate>ID</i18n.Translate>
</th>
<th>
- <i18n.Translate>Description</i18n.Translate>
+ <i18n.Translate>Name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Total products</i18n.Translate>
</th>
<th />
</tr>
@@ -150,27 +138,33 @@ function Table({
<tbody>
{instances.map((i) => {
return (
- <tr key={i.otp_device_id}>
+ <tr key={i.category_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.category_id}
+ </td>
<td
onClick={(): void => onSelect(i)}
style={{ cursor: "pointer" }}
>
- {i.otp_device_id}
+ {i.name}
</td>
<td
onClick={(): void => onSelect(i)}
style={{ cursor: "pointer" }}
>
- {i.otp_device_id}
+ {i.product_count}
</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`}
+ data-tooltip={i18n.str`Delete selected category from the database`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</button>
</div>
</td>
@@ -179,13 +173,13 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
+ {onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices after the last one`}
+ data-tooltip={i18n.str`Load more devices after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load older devices</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
@@ -198,12 +192,12 @@ function EmptyTable(): VNode {
<div class="content has-text-grey has-text-centered">
<p>
<span class="icon is-large">
- <i class="mdi mdi-emoticon-sad mdi-48px" />
+ <i class="mdi mdi-magnify mdi-48px" />
</span>
</p>
<p>
<i18n.Translate>
- There is no devices yet, add more pressing the + sign
+ There is no categories yet, add more pressing the + sign
</i18n.Translate>
</p>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx
new file mode 100644
index 000000000..378fa38d4
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx
@@ -0,0 +1,113 @@
+/*
+ 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,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceCategories } from "../../../../hooks/category.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { CardTable } from "./Table.js";
+
+interface Props {
+ onCreate: () => void;
+ onSelect: (id: string) => void;
+}
+
+export default function ListCategories({ onCreate, onSelect }: Props): VNode {
+ // const [position, setPosition] = useState<string | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { state, lib } = useSessionContext();
+ const result = useInstanceCategories();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <section class="section is-main-section">
+ <CardTable
+ devices={result.body.categories}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(String(e.category_id));
+ }}
+ onDelete={async (e: TalerMerchantApi.CategoryListEntry) => {
+ return lib.instance
+ .deleteCategory(state.token, String(e.category_id))
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Category delete successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not delete the category`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`Could not delete the category`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ }),
+ );
+ }}
+ />
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/Update.stories.tsx
index d6b1d65e0..06ea9d07a 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/Update.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx
new file mode 100644
index 000000000..a08189192
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx
@@ -0,0 +1,162 @@
+/*
+ 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 { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useEffect, 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 { InputArray } from "../../../../components/form/InputArray.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { WithId } from "../../../../declaration.js";
+import {
+ useInstanceProducts
+} from "../../../../hooks/product.js";
+
+type Entity = TalerMerchantApi.CategoryProductList & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ category: Entity;
+}
+export function UpdatePage({ category, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const {
+ state: { token },
+ lib,
+ } = useSessionContext();
+ // FIXME: if the product list is big the will bring a lot of info
+ const inventoryResult = useInstanceProducts();
+
+ const inventory =
+ !inventoryResult ||
+ inventoryResult instanceof TalerError ||
+ inventoryResult.type === "fail"
+ ? []
+ : inventoryResult.body;
+
+ const [state, setState] = useState<
+ Partial<Entity & { product_map: { id: string; description: string }[] }>
+ >({
+ ...category,
+ product_map: [],
+ });
+
+ useEffect(() => {
+ if (!category || !category?.products) return;
+ console.log(category.products);
+ const ps = category.products.map((prod) => {
+ return lib.instance
+ .getProductDetails(token, String(prod.product_id))
+ .then((res) => {
+ return res.type === "fail"
+ ? undefined
+ : { id: String(prod.product_id), description: res.body.description };
+ });
+ });
+ Promise.all(ps).then((all) => {
+ const product_map = all.filter(notEmpty);
+ setState({ ...state, product_map });
+ });
+ }, []);
+
+ const submitForm = () => {
+ const pids = state.product_map?.map((p) => {
+ return { product_id: p.id };
+ });
+ state.products = pids;
+ delete state.product_map;
+ return onUpdate(state as Entity);
+ };
+
+ 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>Id:</i18n.Translate>
+ &nbsp;
+ <b>{category.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}>
+ <Input<Entity>
+ name="name"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`Name of the category`}
+ />
+ <InputArray
+ name="product_map"
+ label={i18n.str`Products`}
+ getSuggestion={async () => {
+ return inventory.map((prod) => {
+ return {
+ description: prod.description,
+ id: prod.id,
+ };
+ });
+ }}
+ help={i18n.str`Search by product description or id`}
+ tooltip={i18n.str`Products that this category will list.`}
+ unique
+ />
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={false}
+ data-tooltip={i18n.str`Confirm operation`}
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
+function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
+ return value !== null && value !== undefined;
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx
new file mode 100644
index 000000000..19352ca3e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx
@@ -0,0 +1,114 @@
+/*
+ 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,
+ TalerError,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useCategoryDetails } from "../../../../hooks/category.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ cid: string;
+}
+export default function UpdateCategory({
+ cid,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const result = useCategoryDetails(cid);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { state, lib } = useSessionContext();
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ category={{
+ ...result.body,
+ id: cid,
+ }}
+ onBack={onBack}
+ onUpdate={async (newInfo) => {
+ return lib.instance
+ .updateCategory(state.token, cid, newInfo)
+ .then((d) => {
+ if (d.type === "ok") {
+ onConfirm();
+ } else {
+ switch (d.case) {
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Could not update category`,
+ type: "ERROR",
+ description: i18n.str`Category id is unknown`,
+ });
+ break;
+ }
+ }
+ }
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`Could not update category`,
+ type: "ERROR",
+ description:
+ error instanceof Error ? error.message : String(error),
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
deleted file mode 100644
index 3168c7cc4..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- 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 { 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
-
-type Entity = TalerMerchantApi.InstanceReconfigurationMessage;
-interface Props {
- onUpdate: () => void;
- onDelete: () => void;
- selected: TalerMerchantApi.QueryInstancesResponse;
-}
-
-function convert(
- from: TalerMerchantApi.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/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
deleted file mode 100644
index e1a7f87f0..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- 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 { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
-import { Loading } from "../../../components/exception/loading.js";
-import { DeleteModal } from "../../../components/modal/index.js";
-import { useSessionContext } from "../../../context/session.js";
-import { useInstanceDetails } from "../../../hooks/instance.js";
-import { LoginPage } from "../../login/index.js";
-import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
-import { DetailPage } from "./DetailPage.js";
-
-interface Props {
- onUpdate: () => void;
- onDelete: () => void;
-}
-
-export default function Detail({
- onUpdate,
- onDelete,
-}: Props): VNode {
- const { state } = useSessionContext();
- const result = useInstanceDetails();
- const [deleting, setDeleting] = useState<boolean>(false);
-
- // const { deleteInstance } = useInstanceAPI();
- const { lib } = useSessionContext();
-
- if (!result) return <Loading />
- if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
- }
- if (result.type === "fail") {
- switch(result.case) {
- case HttpStatusCode.Unauthorized: {
- return <LoginPage />
- }
- case HttpStatusCode.NotFound: {
- return <NotFoundPageOrAdminCreate />;
- }
- default: {
- assertUnreachable(result)
- }
- }
- }
-
-
- return (
- <Fragment>
- <DetailPage
- selected={result.body}
- onUpdate={onUpdate}
- onDelete={() => setDeleting(true)}
- />
- {deleting && (
- <DeleteModal
- element={{ name: result.body.name, id: state.instance }}
- onCancel={() => setDeleting(false)}
- onConfirm={async (): Promise<void> => {
- if (state.status !== "loggedIn") {
- return
- }
- try {
- await lib.instance.deleteCurrentInstance(state.token);
- onDelete();
- } catch (error) {
- //FIXME: show message error
- }
- setDeleting(false);
- }}
- />
- )}
- </Fragment>
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
deleted file mode 100644
index 42cb1cb02..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- 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 { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
-import { FunctionalComponent, h } from "preact";
-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) => (
- <MerchantApiProviderTesting
- value={{
- cancelRequest: () => { },
- changeBackend: () => { },
- config: {
- currency: "ARS",
- version: "1",
- currencies: {
- "ASD": {
- name: "testkudos",
- alt_unit_names: {},
- num_fractional_input_digits: 1,
- num_fractional_normal_digits: 1,
- num_fractional_trailing_zero_digits: 1,
- }
- },
- exchanges: [],
- name: "taler-merchant"
- },
- hints: [],
- lib: {} as any,
- onActivity: (() => { }) as any,
- url: new URL("asdasd"),
- }}
- >
- <Internal {...(props as any)} />
- </MerchantApiProviderTesting>
- );
- 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/merchant-backoffice-ui/src/paths/instance/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts
index 8f06937df..a21ba918e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts
+++ b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts
@@ -14,5 +14,4 @@
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";
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
index 046636b4b..2220b6d1e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
@@ -34,23 +34,29 @@ export default {
export const Example = tests.createExample(TestedComponent, {
status: {
- timeout_kycs: [],
- pending_kycs: [
+ // timeout_kycs: [],
+ kyc_data: [
{
- aml_status: 0,
exchange_url: "http://exchange.taler",
payto_uri: "payto://iban/de123123123" as PaytoString,
- kyc_url: "http://exchange.taler/kyc",
+ // kyc_url: "http://exchange.taler/kyc",
+ exchange_http_status: 0,
+ auth_conflict: false,
+ no_keys: false,
},
{
- aml_status: 1,
+ exchange_http_status: 1,
exchange_url: "http://exchange.taler",
payto_uri: "payto://iban/de123123123" as PaytoString,
+ auth_conflict: false,
+ no_keys: false,
},
{
- aml_status: 2,
+ exchange_http_status: 2,
exchange_url: "http://exchange.taler",
payto_uri: "payto://iban/de123123123" as PaytoString,
+ auth_conflict: false,
+ no_keys: false,
},
],
},
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
index 3eeed1d7b..4e9a708fc 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -19,15 +19,27 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ encodeCrock,
+ hashPaytoUri,
+ hashWire,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
export interface Props {
- status: TalerMerchantApi.AccountKycRedirects;
+ status: TalerMerchantApi.MerchantAccountKycRedirectsResponse;
+ // onGetInfo: (url: string, token: AccessToken) => void;
+ onShowInstructions: (toAccounts: string[], fromAccount: string) => void;
}
-export function ListPage({ status }: Props): VNode {
+export function ListPage({
+ status,
+ // onGetInfo,
+ onShowInstructions,
+}: Props): VNode {
const { i18n } = useTranslationContext();
return (
@@ -46,8 +58,12 @@ export function ListPage({ status }: Props): VNode {
<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} />
+ {status.kyc_data.length > 0 ? (
+ <PendingTable
+ entries={status.kyc_data}
+ // onGetInfo={onGetInfo}
+ onShowInstructions={onShowInstructions}
+ />
) : (
<EmptyTable />
)}
@@ -55,44 +71,24 @@ export function ListPage({ status }: Props): VNode {
</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: TalerMerchantApi.MerchantAccountKycRedirect[];
+ // onGetInfo: (url: string, token: AccessToken) => void;
+ onShowInstructions: (toAccounts: string[], fromAccount: string) => void;
}
interface TimedOutTableProps {
entries: TalerMerchantApi.ExchangeKycTimeout[];
}
-function PendingTable({ entries }: PendingTableProps): VNode {
+function PendingTable({
+ entries,
+ onShowInstructions,
+ // onGetInfo,
+}: PendingTableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
@@ -103,7 +99,7 @@ function PendingTable({ entries }: PendingTableProps): VNode {
<i18n.Translate>Exchange</i18n.Translate>
</th>
<th>
- <i18n.Translate>Target account</i18n.Translate>
+ <i18n.Translate>Account</i18n.Translate>
</th>
<th>
<i18n.Translate>Reason</i18n.Translate>
@@ -112,38 +108,37 @@ function PendingTable({ entries }: PendingTableProps): VNode {
</thead>
<tbody>
{entries.map((e, i) => {
- if (e.kyc_url === undefined) {
- // blocked by AML
+ if (e.payto_kycauths === undefined) {
+ const spa = new URL(`kyc-spa/${e.access_token}`, e.exchange_url)
+ .href;
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>
- ) : (
+ <a href={spa} target="_black" rel="noreferrer">
<i18n.Translate>
- The account is frozen due to the anti-money laundering
- rules. Contact the exchange service provider for further
- instructions.
+ Pending KYC process, click here to complete
</i18n.Translate>
- )}
+ </a>
</td>
</tr>
);
} else {
- // blocked by KYC
+ const accounts = e.payto_kycauths;
return (
<tr key={i}>
<td>{e.exchange_url}</td>
- <td>{e.payto_uri}</td>
+ <td
+ onClick={() => onShowInstructions(accounts, e.payto_uri)}
+ style={{ cursor: "pointer" }}
+ >
+ {e.payto_uri}
+ </td>
<td>
- <a href={e.kyc_url} target="_black" rel="noreferrer">
+ <a href={e.access_token} target="_black" rel="noreferrer">
<i18n.Translate>
- Pending KYC process, click here to complete
+ The exchange require a account verification.
</i18n.Translate>
</a>
</td>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
index ed0e1220f..9f65be0c3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -19,21 +19,34 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerExchangeHttpClient,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
import { ListPage } from "./ListPage.js";
+import { useState } from "preact/hooks";
+import { ValidBankAccount } from "../../../../components/modal/index.js";
interface Props {
+ // onGetInfo: (id: string) => void;
+ // onShowInstructions: (id: string) => void;
}
export default function ListKYC(_p: Props): VNode {
const result = useInstanceKYCDetails();
- if (!result) return <Loading />
+ const [showingInstructions, setShowingInstructions] = useState<
+ { toAccounts: string[]; fromAccount: string } | undefined
+ >(undefined);
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
/**
* This component just render known kyc requirements.
@@ -42,28 +55,31 @@ export default function ListKYC(_p: Props): VNode {
if (result.type === "fail") {
switch (result.case) {
case HttpStatusCode.GatewayTimeout: {
- return <div />
+ return <div />;
}
case HttpStatusCode.BadGateway: {
- const status = result.body;
-
- if (!status) {
- return <div>no kyc required</div>;
- }
- return <ListPage status={status} />;
-
+ break;
+ // return (
+ // <ListPage
+ // status={result.body}
+ // onGetInfo={_p.onGetInfo}
+ // onShowInstructions={() => {
+ // setShowingInstructions(true)
+ // }}
+ // />
+ // );
}
case HttpStatusCode.ServiceUnavailable: {
- return <div />
+ return <div />;
}
case HttpStatusCode.Unauthorized: {
- return <div />
+ return <div />;
}
case HttpStatusCode.NotFound: {
return <div />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -72,5 +88,28 @@ export default function ListKYC(_p: Props): VNode {
if (!status) {
return <div>no kyc required</div>;
}
- return <ListPage status={status} />;
+ return (
+ <Fragment>
+ {showingInstructions !== undefined ? (
+ <Fragment>
+ <ValidBankAccount
+ origin={parsePaytoUri(showingInstructions.fromAccount)!}
+ targets={showingInstructions.toAccounts.map(
+ (d) => parsePaytoUri(d)!,
+ )}
+ onCancel={() => setShowingInstructions(undefined)}
+ />
+ </Fragment>
+ ) : undefined}
+ <ListPage
+ status={status}
+ // onGetInfo={async (exchange, ac) => {
+ // new URL()
+ // }}
+ onShowInstructions={(toAccounts, fromAccount) => {
+ setShowingInstructions({ toAccounts, fromAccount });
+ }}
+ />
+ </Fragment>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
index 7be3d23f6..727db4f68 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -27,9 +27,7 @@ import {
TalerMerchantApi,
TalerProtocolDuration,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format, isFuture } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -52,6 +50,7 @@ import { useSessionContext } from "../../../../context/session.js";
import { usePreference } from "../../../../hooks/preference.js";
import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { WithId } from "../../../../declaration.js";
interface Props {
onCreate: (d: TalerMerchantApi.PostOrderRequest) => void;
@@ -75,6 +74,10 @@ function with_defaults(
const defaultWireDeadline = Duration.fromTalerProtocolDuration(
config.default_wire_transfer_delay,
);
+ const defaultRefundDeadline = Duration.min(
+ defaultWireDeadline,
+ Duration.fromSpec({ days: 15 }),
+ );
return {
inventoryProducts: {},
@@ -84,7 +87,7 @@ function with_defaults(
max_fee: undefined,
createToken: true,
pay_deadline: defaultPayDeadline,
- refund_deadline: defaultPayDeadline,
+ refund_deadline: defaultRefundDeadline,
wire_transfer_deadline: defaultWireDeadline,
},
shipping: {},
@@ -114,7 +117,7 @@ interface Payments {
refund_deadline: Duration;
pay_deadline: Duration;
wire_transfer_deadline: Duration;
- auto_refund_deadline: Duration;
+ auto_refund_deadline?: Duration;
max_fee?: string;
createToken: boolean;
minimum_age?: number;
@@ -138,7 +141,7 @@ export function CreatePage({
const instance_default = with_defaults(instanceConfig, config.currency);
const [value, valueHandler] = useState(instance_default);
const zero = Amounts.zeroOfCurrency(config.currency);
- const [settings, updateSettings] = usePreference();
+ const [pref, updatePrefs] = usePreference();
const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
@@ -148,15 +151,15 @@ export function CreatePage({
? undefined
: Amounts.parse(value.pricing.order_price);
- const errors: FormErrors<Entity> = {
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
pricing: undefinedIfEmpty({
- summary: !value.pricing?.summary ? i18n.str`required` : undefined,
+ summary: !value.pricing?.summary ? i18n.str`Required` : undefined,
order_price: !value.pricing?.order_price
- ? i18n.str`required`
+ ? i18n.str`Required`
: !parsedPrice
- ? i18n.str`not valid`
+ ? i18n.str`Invalid`
: Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
+ ? i18n.str`Must be greater than 0`
: undefined,
}),
payments: undefinedIfEmpty({
@@ -167,58 +170,59 @@ export function CreatePage({
value.payments.refund_deadline,
value.payments.pay_deadline,
) === -1
- ? i18n.str`refund deadline cannot be before pay deadline`
+ ? i18n.str`Refund deadline can't 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`
+ ? i18n.str`Wire transfer deadline can't be before refund deadline`
: undefined,
pay_deadline: !value.payments?.pay_deadline
- ? i18n.str`required`
+ ? 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`
+ ? i18n.str`Wire transfer deadline can't be before pay deadline`
: undefined,
wire_transfer_deadline: !value.payments?.wire_transfer_deadline
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
auto_refund_deadline: !value.payments?.auto_refund_deadline
? undefined
: !value.payments?.refund_deadline
- ? i18n.str`should have a refund deadline`
+ ? i18n.str`Must 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`
+ ? i18n.str`Auto refund can't 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`
+ ? i18n.str`Must be in the future`
: undefined,
}),
- };
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ });
+ const hasErrors = errors !== undefined;
const submit = (): void => {
- const order = value as any; //schema.cast(value);
+ const order = value;
+ const price = order.pricing?.order_price as AmountString | undefined;
+ const summary = order.pricing?.summary;
if (!value.payments) return;
if (!value.shipping) return;
+ if (!price || !summary) return;
const request: TalerMerchantApi.PostOrderRequest = {
order: {
- amount: order.pricing.order_price,
- summary: order.pricing.summary,
+ amount: price,
+ summary: summary,
products: productList,
extra: undefinedIfEmpty(value.extra),
pay_deadline: AbsoluteTime.toProtocolTimestamp(
@@ -339,7 +343,9 @@ export function CreatePage({
const minAgeByProducts = inventoryList.reduce(
(cur, prev) =>
- !prev.product.minimum_age || cur > prev.product.minimum_age ? cur : prev.product.minimum_age,
+ !prev.product.minimum_age || cur > prev.product.minimum_age
+ ? cur
+ : prev.product.minimum_age,
0,
);
@@ -360,9 +366,9 @@ export function CreatePage({
<div class="tabs is-toggle is-fullwidth is-small">
<ul>
<li
- class={!settings.advanceOrderMode ? "is-active" : ""}
+ class={!pref.advanceOrderMode ? "is-active" : ""}
onClick={() => {
- updateSettings("advanceOrderMode", false);
+ updatePrefs("advanceOrderMode", false);
}}
>
<a>
@@ -372,9 +378,9 @@ export function CreatePage({
</a>
</li>
<li
- class={settings.advanceOrderMode ? "is-active" : ""}
+ class={pref.advanceOrderMode ? "is-active" : ""}
onClick={() => {
- updateSettings("advanceOrderMode", true);
+ updatePrefs("advanceOrderMode", true);
}}
>
<a>
@@ -395,8 +401,10 @@ export function CreatePage({
alternative={
allProducts.length > 0 && (
<p>
- {allProducts.length} products with a total price of{" "}
- {totalAsString}.
+ <i18n.Translate>
+ {allProducts.length} products with a total price of{" "}
+ {totalAsString}.
+ </i18n.Translate>
</p>
)
}
@@ -408,7 +416,7 @@ export function CreatePage({
inventory={instanceInventory}
/>
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<NonInventoryProductFrom
productToEdit={editingProduct}
onAddProduct={(p) => {
@@ -450,7 +458,7 @@ export function CreatePage({
name="pricing.products_price"
label={i18n.str`Total price`}
readonly
- tooltip={i18n.str`total product price added up`}
+ tooltip={i18n.str`Total product price added up`}
/>
<InputCurrency
name="pricing.order_price"
@@ -470,7 +478,7 @@ export function CreatePage({
<InputCurrency
name="pricing.order_price"
label={i18n.str`Order price`}
- tooltip={i18n.str`final order price`}
+ tooltip={i18n.str`Final order price`}
/>
)}
@@ -481,7 +489,7 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputGroup
name="shipping"
label={i18n.str`Shipping and Fulfillment`}
@@ -496,7 +504,7 @@ export function CreatePage({
<InputGroup
name="shipping.delivery_location"
label={i18n.str`Location`}
- tooltip={i18n.str`address where the products will be delivered`}
+ tooltip={i18n.str`Address where the products will be delivered`}
>
<InputLocation name="shipping.delivery_location" />
</InputGroup>
@@ -509,13 +517,13 @@ export function CreatePage({
</InputGroup>
)}
- {(settings.advanceOrderMode || requiresSomeTalerOptions) && (
+ {(pref.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) && (
+ {(pref.advanceOrderMode || noDefault_payDeadline) && (
<InputDuration
name="payments.pay_deadline"
label={i18n.str`Payment time`}
@@ -541,13 +549,13 @@ export function CreatePage({
valueHandler(c);
}}
>
- <i18n.Translate>default</i18n.Translate>
+ <i18n.Translate>Default</i18n.Translate>
</button>
</span>
}
/>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputDuration
name="payments.refund_deadline"
label={i18n.str`Refund time`}
@@ -574,13 +582,13 @@ export function CreatePage({
});
}}
>
- <i18n.Translate>default</i18n.Translate>
+ <i18n.Translate>Default</i18n.Translate>
</button>
</span>
}
/>
)}
- {(settings.advanceOrderMode || noDefault_wireDeadline) && (
+ {(pref.advanceOrderMode || noDefault_wireDeadline) && (
<InputDuration
name="payments.wire_transfer_deadline"
label={i18n.str`Wire transfer time`}
@@ -608,13 +616,13 @@ export function CreatePage({
});
}}
>
- <i18n.Translate>default</i18n.Translate>
+ <i18n.Translate>Default</i18n.Translate>
</button>
</span>
}
/>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputDuration
name="payments.auto_refund_deadline"
label={i18n.str`Auto-refund time`}
@@ -628,21 +636,21 @@ export function CreatePage({
/>
)}
- {settings.advanceOrderMode && (
+ {pref.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 && (
+ {pref.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 && (
+ {pref.advanceOrderMode && (
<InputNumber
name="payments.minimum_age"
label={i18n.str`Minimum age required`}
@@ -657,7 +665,7 @@ export function CreatePage({
</InputGroup>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputGroup
name="extra"
label={i18n.str`Additional information`}
@@ -730,7 +738,7 @@ export function CreatePage({
e.preventDefault();
}}
>
- add
+ <i18n.Translate>Add</i18n.Translate>
</button>
</div>
</InputGroup>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
index 861114014..0dc125706 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -19,7 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -32,6 +37,7 @@ import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CreatePage } from "./CreatePage.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
export type Entity = {
request: TalerMerchantApi.PostOrderRequest;
@@ -41,24 +47,22 @@ interface Props {
onBack?: () => void;
onConfirm: (id: string) => void;
}
-export default function OrderCreate({
- onConfirm,
- onBack,
-}: Props): VNode {
- const { lib } = useSessionContext();
+export default function OrderCreate({ onConfirm, onBack }: Props): VNode {
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { state } = useSessionContext();
const detailsResult = useInstanceDetails();
+ // FIXME: if the product list is big the will bring a lot of info
const inventoryResult = useInstanceProducts();
+ const { i18n } = useTranslationContext();
- if (!detailsResult) return <Loading />
+ if (!detailsResult) return <Loading />;
if (detailsResult instanceof TalerError) {
- return <ErrorLoadingMerchant error={detailsResult} />
+ return <ErrorLoadingMerchant error={detailsResult} />;
}
if (detailsResult.type === "fail") {
switch (detailsResult.case) {
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
case HttpStatusCode.NotFound: {
return <NotFoundPageOrAdminCreate />;
@@ -68,9 +72,9 @@ export default function OrderCreate({
}
}
}
- if (!inventoryResult) return <Loading />
+ if (!inventoryResult) return <Loading />;
if (inventoryResult instanceof TalerError) {
- return <ErrorLoadingMerchant error={inventoryResult} />
+ return <ErrorLoadingMerchant error={inventoryResult} />;
}
if (inventoryResult.type === "fail") {
switch (inventoryResult.case) {
@@ -78,7 +82,7 @@ export default function OrderCreate({
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(inventoryResult);
@@ -93,22 +97,51 @@ export default function OrderCreate({
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.PostOrderRequest) => {
- lib.instance.createOrder(state.token, request)
- .then((r) => {
- if (r.type === "ok") {
- return onConfirm(r.body.order_id)
+ lib.instance
+ .createOrder(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ return onConfirm(resp.body.order_id);
} else {
- setNotif({
- message: "could not create order",
- type: "ERROR",
- });
+ switch (resp.case) {
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: i18n.str`No exchange would accept a payment because of KYC requirements.`
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ case HttpStatusCode.Gone: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: i18n.str`No more stock for product with id "${resp.body.product_id}".`
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp)
+ }
+ }
}
})
.catch((error) => {
setNotif({
- message: "could not create order",
+ message: i18n.str`Could not create order`,
type: "ERROR",
- description: error.message,
+ description:
+ error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
index 498ea83e3..2ffb4203d 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -75,13 +75,13 @@ function ContractTerms({ value }: { value: CT }) {
readonly
name="summary"
label={i18n.str`Summary`}
- tooltip={i18n.str`human-readable description of the whole purchase`}
+ 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`}
+ tooltip={i18n.str`Total price for the transaction`}
/>
{value.fulfillment_url && (
<Input<CT>
@@ -95,43 +95,43 @@ function ContractTerms({ value }: { value: CT }) {
readonly
name="max_fee"
label={i18n.str`Max fee`}
- tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
+ 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`}
+ 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`}
+ 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`}
+ 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`}
+ 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`}
+ 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`}
+ tooltip={i18n.str`Where the order will be delivered`}
>
<InputLocation name="payments.delivery_location" />
</InputGroup>
@@ -140,13 +140,13 @@ function ContractTerms({ value }: { value: 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`}
+ 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`}
+ tooltip={i18n.str`Extra data that is only interpreted by the merchant frontend`}
/>
</FormProvider>
</InputGroup>
@@ -221,7 +221,7 @@ function ClaimedPage({
<div class="level-item">
<i18n.Translate>Order</i18n.Translate> #{id}
<div class="tag is-info ml-4">
- <i18n.Translate>claimed</i18n.Translate>
+ <i18n.Translate>Claimed</i18n.Translate>
</div>
</div>
</div>
@@ -248,7 +248,7 @@ function ClaimedPage({
>
<p>
<b>
- <i18n.Translate>claimed at</i18n.Translate>:
+ <i18n.Translate>Claimed at</i18n.Translate>:
</b>{" "}
{order.contract_terms.timestamp.t_s === "never"
? "never"
@@ -458,16 +458,16 @@ function PaidPage({
<div class="level-item">
<i18n.Translate>Order</i18n.Translate> #{id}
<div class="tag is-success ml-4">
- <i18n.Translate>paid</i18n.Translate>
+ <i18n.Translate>Paid</i18n.Translate>
</div>
{order.wired ? (
<div class="tag is-success ml-4">
- <i18n.Translate>wired</i18n.Translate>
+ <i18n.Translate>Wired</i18n.Translate>
</div>
) : null}
{order.refunded ? (
<div class="tag is-danger ml-4">
- <i18n.Translate>refunded</i18n.Translate>
+ <i18n.Translate>Refunded</i18n.Translate>
</div>
) : null}
</div>
@@ -487,8 +487,8 @@ function PaidPage({
class="has-tooltip-left"
data-tooltip={
refundable
- ? i18n.str`refund order`
- : i18n.str`not refundable`
+ ? i18n.str`Refund order`
+ : i18n.str`Not refundable`
}
>
<button
@@ -496,7 +496,7 @@ function PaidPage({
disabled={!refundable}
onClick={() => onRefund(id)}
>
- <i18n.Translate>refund</i18n.Translate>
+ <i18n.Translate>Refund</i18n.Translate>
</button>
</span>
</div>
@@ -638,7 +638,7 @@ function UnpaidPage({
</h1>
</div>
<div class="tag is-dark">
- <i18n.Translate>unpaid</i18n.Translate>
+ <i18n.Translate>Unpaid</i18n.Translate>
</div>
</div>
</div>
@@ -656,7 +656,7 @@ function UnpaidPage({
>
<p>
<b>
- <i18n.Translate>pay at</i18n.Translate>:
+ <i18n.Translate>Pay at</i18n.Translate>:
</b>{" "}
<a
href={order.order_status_url}
@@ -668,7 +668,7 @@ function UnpaidPage({
</p>
<p>
<b>
- <i18n.Translate>created at</i18n.Translate>:
+ <i18n.Translate>Created at</i18n.Translate>:
</b>{" "}
{order.creation_time.t_s === "never"
? "never"
@@ -693,13 +693,13 @@ function UnpaidPage({
readonly
name="summary"
label={i18n.str`Summary`}
- tooltip={i18n.str`human-readable description of the whole purchase`}
+ 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`}
+ tooltip={i18n.str`Total price for the transaction`}
/>
<Input<Unpaid>
name="order_status"
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
index b28e59b29..c7edce834 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -18,9 +18,7 @@ import {
TalerError,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -41,8 +39,7 @@ export interface Props {
export default function Update({ oid, onBack }: Props): VNode {
const result = useOrderDetails(oid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib: api } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const { i18n } = useTranslationContext();
@@ -64,7 +61,7 @@ export default function Update({ oid, onBack }: Props): VNode {
);
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -83,19 +80,47 @@ export default function Update({ oid, onBack }: Props): VNode {
if (state.status !== "loggedIn") {
return;
}
- api.instance
+ lib.instance
.addRefund(state.token, id, value)
- .then(() =>
- setNotif({
- message: i18n.str`refund created successfully`,
- type: "SUCCESS",
- }),
- )
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Refund created successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ setNotif({
+ message: i18n.str`Could not create the refund`,
+ type: "ERROR",
+ description: i18n.str`There are pending KYC requirements.`
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ setNotif({
+ message: i18n.str`Could not create the refund`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp)
+ }
+ }
+ }
+ })
.catch((error) =>
setNotif({
- message: i18n.str`could not create the refund`,
+ message: i18n.str`Could not create the refund`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
}),
);
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
index 408bc0c0a..fd1c9fa30 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -27,6 +27,7 @@ import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js";
import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js";
import { CardTable } from "./Table.js";
+import { WithId } from "../../../../declaration.js";
export interface ListPageProps {
onShowAll: () => void;
@@ -79,7 +80,7 @@ export function ListPage({
isWiredActive,
}: ListPageProps): VNode {
const { i18n } = useTranslationContext();
- const dateTooltip = i18n.str`select date to show nearby orders`;
+ const dateTooltip = i18n.str`Select date to show nearby orders`;
const [pickDate, setPickDate] = useState(false);
const [settings] = usePreference();
@@ -92,7 +93,7 @@ export function ListPage({
<li class={isNotPaidActive}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`only show paid orders`}
+ data-tooltip={i18n.str`Only show paid orders`}
>
<a onClick={onShowNotPaid}>
<i18n.Translate>New</i18n.Translate>
@@ -102,7 +103,7 @@ export function ListPage({
<li class={isPaidActive}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`only show paid orders`}
+ data-tooltip={i18n.str`Only show paid orders`}
>
<a onClick={onShowPaid}>
<i18n.Translate>Paid</i18n.Translate>
@@ -112,7 +113,7 @@ export function ListPage({
<li class={isRefundedActive}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`only show orders with refunds`}
+ data-tooltip={i18n.str`Only show orders with refunds`}
>
<a onClick={onShowRefunded}>
<i18n.Translate>Refunded</i18n.Translate>
@@ -122,7 +123,7 @@ export function ListPage({
<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`}
+ 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>
@@ -132,7 +133,7 @@ export function ListPage({
<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`}
+ 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>
@@ -142,7 +143,7 @@ export function ListPage({
<li class={isAllActive}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`remove all filters`}
+ data-tooltip={i18n.str`Remove all filters`}
>
<a onClick={onShowAll}>
<i18n.Translate>All</i18n.Translate>
@@ -160,7 +161,7 @@ export function ListPage({
<a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
<span
class="icon"
- data-tooltip={i18n.str`clear date filter`}
+ data-tooltip={i18n.str`Clear date filter`}
>
<i class="mdi mdi-close" />
</span>
@@ -174,7 +175,7 @@ export function ListPage({
type="text"
readonly
value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))}
- placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
+ placeholder={i18n.str`Jump to date (${dateFormatForSettings(settings)})`}
onClick={() => {
setPickDate(true);
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
index 5ece34409..74bfe7939 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -41,6 +41,7 @@ import {
usePreference,
} from "../../../../hooks/preference.js";
import { mergeRefunds } from "../../../../utils/amount.js";
+import { WithId } from "../../../../declaration.js";
type Entity = TalerMerchantApi.OrderHistoryEntry & WithId;
interface Props {
@@ -79,7 +80,7 @@ export function CardTable({
<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`}>
+ <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" />
@@ -136,7 +137,7 @@ function Table({
<div class="table-container">
{onLoadMoreBefore && (
<button class="button is-fullwidth" onClick={onLoadMoreBefore}>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-striped is-hoverable is-fullwidth">
@@ -210,9 +211,9 @@ function Table({
</table>
{onLoadMoreAfter && (
<button class="button is-fullwidth"
- data-tooltip={i18n.str`load more orders after the last one`}
+ data-tooltip={i18n.str`Load more orders after the last one`}
onClick={onLoadMoreAfter}>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
@@ -276,20 +277,20 @@ export function RefundModal({
: orderPrice;
const isRefundable = Amounts.isNonZero(totalRefundable);
- const duplicatedText = i18n.str`duplicated`;
+ const duplicatedText = i18n.str`Duplicated`;
const errors: FormErrors<State> = {
- mainReason: !form.mainReason ? i18n.str`required` : undefined,
+ mainReason: !form.mainReason ? i18n.str`Required` : undefined,
description:
!form.description && form.mainReason !== duplicatedText
- ? i18n.str`required`
+ ? i18n.str`Required`
: undefined,
refund: !form.refund
- ? i18n.str`required`
+ ? i18n.str`Required`
: !Amounts.parse(form.refund)
- ? i18n.str`invalid format`
+ ? i18n.str`Invalid`
: Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
- ? i18n.str`this value exceed the refundable amount`
+ ? i18n.str`This value exceed the refundable amount`
: undefined,
};
const hasErrors = Object.keys(errors).some(
@@ -334,13 +335,13 @@ export function RefundModal({
<thead>
<tr>
<th>
- <i18n.Translate>date</i18n.Translate>
+ <i18n.Translate>Date</i18n.Translate>
</th>
<th>
- <i18n.Translate>amount</i18n.Translate>
+ <i18n.Translate>Amount</i18n.Translate>
</th>
<th>
- <i18n.Translate>reason</i18n.Translate>
+ <i18n.Translate>Reason</i18n.Translate>
</th>
</tr>
</thead>
@@ -377,7 +378,7 @@ export function RefundModal({
<InputCurrency<State>
name="refund"
label={i18n.str`Refund`}
- tooltip={i18n.str`amount to be refunded`}
+ tooltip={i18n.str`Amount to be refunded`}
>
<i18n.Translate>Max refundable:</i18n.Translate>{" "}
{Amounts.stringify(totalRefundable)}
@@ -388,16 +389,16 @@ export function RefundModal({
values={[
i18n.str`Choose one...`,
duplicatedText,
- i18n.str`requested by the customer`,
- i18n.str`other`,
+ i18n.str`Requested by the customer`,
+ i18n.str`Other`,
]}
- tooltip={i18n.str`why this order is being refunded`}
+ 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`}
+ tooltip={i18n.str`More information to give context`}
/>
) : undefined}
</FormProvider>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
index 8a1f85b1c..787512e2a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -26,9 +26,7 @@ import {
TalerMerchantApi,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -64,27 +62,26 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
const result = useInstanceOrders(filter, (d) =>
setFilter({ ...filter, position: d }),
);
- const { lib } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { state } = useSessionContext();
if (!result) return <Loading />;
if (result instanceof TalerError) {
return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.NotFound: {
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -113,8 +110,8 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
return resp.type === "ok";
}}
onSelect={onSelect}
- description={i18n.str`jump to order with the given product ID`}
- placeholder={i18n.str`order id`}
+ description={i18n.str`Jump to order with the given product ID`}
+ placeholder={i18n.str`Order id`}
/>
<ListPage
@@ -160,17 +157,47 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode {
onConfirm={(value) => {
lib.instance
.addRefund(state.token, orderToBeRefunded.order_id, value)
- .then(() =>
- setNotif({
- message: i18n.str`refund created successfully`,
- type: "SUCCESS",
- }),
- )
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Refund created successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ setNotif({
+ message: i18n.str`Could not create the refund`,
+ type: "ERROR",
+ description: i18n.str`There are pending KYC requirements.`
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ setNotif({
+ message: i18n.str`Could not create the refund`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp)
+ }
+
+ }
+
+ }
+ })
.catch((error) =>
setNotif({
- message: i18n.str`could not create the refund`,
+ message: i18n.str`Could not create the refund`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
}),
)
.then(() => setOrderToBeRefunded(undefined));
@@ -208,7 +235,7 @@ function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode {
);
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
index a16817bab..bd024fb38 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -35,6 +35,7 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = TalerMerchantApi.OtpDeviceAddDetails;
@@ -53,34 +54,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const [showKey, setShowKey] = useState(false);
- const errors: FormErrors<Entity> = {
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
otp_device_id: !state.otp_device_id
- ? i18n.str`required`
+ ? i18n.str`Required`
: !/[a-zA-Z0-9]*/.test(state.otp_device_id)
- ? i18n.str`no valid. only characters and numbers`
+ ? i18n.str`Invalid. Only characters and numbers`
: undefined,
- otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined,
+ otp_algorithm: !state.otp_algorithm ? i18n.str`Required` : undefined,
otp_key: !state.otp_key
- ? i18n.str`required`
+ ? i18n.str`Required`
: !isRfc3548Base32Charset(state.otp_key)
- ? i18n.str`just letters and numbers from 2 to 7`
+ ? i18n.str`Just letters and numbers from 2 to 7`
: state.otp_key.length !== 32
- ? i18n.str`size of the key should be 32`
+ ? i18n.str`Size of the key must be 32`
: undefined,
otp_device_description: !state.otp_device_description
- ? i18n.str`required`
+ ? i18n.str`Required`
: !/[a-zA-Z0-9]*/.test(state.otp_device_description)
- ? i18n.str`no valid. only characters and numbers`
+ ? i18n.str`Invalid. Only characters and numbers`
: undefined,
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const submitForm = () => {
if (hasErrors) return Promise.reject();
- return onCreate(state as any);
+ return onCreate(state as TalerMerchantApi.OtpDeviceAddDetails);
};
return (
@@ -112,14 +111,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
toStr={(v) => algorithmsNames[v]}
fromStr={(v) => Number(v)}
/>
- {state.otp_algorithm ? (
+ {state.otp_algorithm ? (
<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"
+ help={i18n.str`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={() => {
@@ -136,7 +135,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
}
side={
<button
- data-tooltip={i18n.str`generate random secret key`}
+ data-tooltip={i18n.str`Generate random secret key`}
class="button is-info mr-3"
onClick={(e) => {
setState((s) => ({
@@ -146,7 +145,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
e.preventDefault();
}}
>
- <i18n.Translate>random</i18n.Translate>
+ <i18n.Translate>Random</i18n.Translate>
</button>
}
/>
@@ -165,7 +164,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
index 8ab0e1f26..476cd3ba9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
@@ -36,14 +36,14 @@ interface Props {
}
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { lib: api } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null)
+ const [created, setCreated] =
+ useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null);
if (created) {
- return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />;
}
return (
@@ -51,16 +51,29 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: Entity) => {
- return api.instance.addOtpDevice(state.token, request)
- .then((d) => {
- setCreated(request)
+ onCreate={async (request: Entity) => {
+ return lib.instance
+ .addOtpDevice(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Device added successfully`,
+ type: "SUCCESS",
+ });
+ setCreated(request);
+ } else {
+ setNotif({
+ message: i18n.str`Could not add device`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
})
.catch((error) => {
setNotif({
- message: i18n.str`could not create device`,
+ message: i18n.str`Could not add device`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
deleted file mode 100644
index 49032c80e..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- 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 { FunctionalComponent, h } from "preact";
-import { ListPage as TestedComponent } from "./ListPage.js";
-
-export default {
- title: "Pages/OtpDevices/List",
- component: TestedComponent,
-};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
deleted file mode 100644
index 8ca0a9c58..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { CardTable } from "./Table.js";
-
-export interface Props {
- devices: TalerMerchantApi.OtpDeviceEntry[];
- onLoadMoreBefore?: () => void;
- onLoadMoreAfter?: () => void;
- onCreate: () => void;
- onDelete: (e: TalerMerchantApi.OtpDeviceEntry) => void;
- onSelect: (e: TalerMerchantApi.OtpDeviceEntry) => void;
-}
-
-export function ListPage({
- devices,
- onCreate,
- onDelete,
- onSelect,
- onLoadMoreBefore,
- onLoadMoreAfter,
-}: Props): VNode {
-
- 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}
- onLoadMoreAfter={onLoadMoreAfter}
- />
- </section>
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
index e4206ff7d..cdba229f6 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -59,7 +59,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new devices`}
+ data-tooltip={i18n.str`Add new devices`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -114,10 +114,10 @@ function Table({
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices before the first one`}
+ data-tooltip={i18n.str`Load more devices before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -152,10 +152,10 @@ function Table({
<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`}
+ data-tooltip={i18n.str`Delete selected devices from the database`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</button>
</div>
</td>
@@ -167,10 +167,10 @@ function Table({
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices after the last one`}
+ data-tooltip={i18n.str`Load more devices after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
index b6a077863..f566ee2c2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -23,11 +23,9 @@ import {
HttpStatusCode,
TalerError,
TalerMerchantApi,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -38,7 +36,7 @@ import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-import { ListPage } from "./ListPage.js";
+import { CardTable } from "./Table.js";
interface Props {
onCreate: () => void;
@@ -49,8 +47,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
// const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useInstanceOtpDevices();
if (!result) return <Loading />;
@@ -63,7 +60,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -75,32 +72,42 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
<Fragment>
<NotificationCard notification={notif} />
- <ListPage
- devices={result.body.otp_devices}
- onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
- onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
- onCreate={onCreate}
- onSelect={(e) => {
- onSelect(e.otp_device_id);
- }}
- onDelete={(e: TalerMerchantApi.OtpDeviceEntry) => {
- return lib.instance
- .deleteOtpDevice(state.token, 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,
- }),
- );
- }}
- />
+ <section class="section is-main-section">
+ <CardTable
+ devices={result.body.otp_devices}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={async (e: TalerMerchantApi.OtpDeviceEntry) => {
+ return lib.instance
+ .deleteOtpDevice(state.token, e.otp_device_id)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Device delete successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not delete the device`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`Could not delete the device`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ }),
+ );
+ }}
+ />
+ </section>
</Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
index 35d67cbc6..68fbf371a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx
@@ -19,18 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { randomRfc3548Base32Key, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ randomRfc3548Base32Key,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
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 { FormProvider } from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { WithId } from "../../../../declaration.js";
type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId;
@@ -47,15 +48,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
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 onUpdate(state as Entity);
};
return (
@@ -67,7 +61,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- Device: <b>{device.id}</b>
+ <i18n.Translate>Device:</i18n.Translate>
+ <b>{device.id}</b>
</span>
</div>
</div>
@@ -79,11 +74,7 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
<section class="section is-main-section">
<div class="columns">
<div class="column is-four-fifths">
- <FormProvider
- object={state}
- valueHandler={setState}
- errors={errors}
- >
+ <FormProvider object={state} valueHandler={setState}>
<Input<Entity>
name="otp_device_description"
label={i18n.str`Description`}
@@ -106,8 +97,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
inputType={showKey ? "text" : "password"}
help={
state.otp_key === undefined
- ? "Not modified"
- : "Be sure to be very hard to guess or use the random generator"
+ ? i18n.str`Not modified`
+ : i18n.str`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()}
@@ -131,25 +122,25 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
side={
state.otp_key === undefined ? (
<button
- onClick={(e) => {
+ onClick={() => {
setState((s) => ({ ...s, otp_key: "" }));
}}
class="button"
>
- change key
+ <i18n.Translate>Change key</i18n.Translate>
</button>
) : (
<button
- data-tooltip={i18n.str`generate random secret key`}
+ data-tooltip={i18n.str`Generate random secret key`}
class="button is-info mr-3"
- onClick={(e) => {
+ onClick={() => {
setState((s) => ({
...s,
otp_key: randomRfc3548Base32Key(),
}));
}}
>
- <i18n.Translate>random</i18n.Translate>
+ <i18n.Translate>Random</i18n.Translate>
</button>
)
}
@@ -165,12 +156,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
</button>
)}
<AsyncButton
- disabled={hasErrors}
- data-tooltip={
- hasErrors
- ? i18n.str`Need to complete marked fields`
- : "confirm operation"
- }
+ disabled={false}
+ data-tooltip={i18n.str`Confirm operation`}
onClick={submitForm}
>
<i18n.Translate>Confirm</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
index 99edb95c3..8f9997cfb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -40,6 +40,7 @@ import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CreatedSuccessfully } from "../create/CreatedSuccessfully.js";
import { UpdatePage } from "./UpdatePage.js";
+import { WithId } from "../../../../declaration.js";
export type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId;
@@ -57,8 +58,7 @@ export default function UpdateValidator({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const [keyUpdated, setKeyUpdated] =
useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null);
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const { i18n } = useTranslationContext();
@@ -135,9 +135,9 @@ export default function UpdateValidator({
})
.catch((error) => {
setNotif({
- message: i18n.str`could not update template`,
+ message: i18n.str`Could not update template`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
deleted file mode 100644
index 2b6ebed45..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- 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 { 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/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
index 9de5cae78..f7c462647 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -34,8 +34,7 @@ interface Props {
onConfirm: () => void;
}
export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -45,13 +44,28 @@ export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.ProductAddDetail) => {
- return lib.instance.addProduct(state.token, request)
- .then(() => onConfirm())
+ return lib.instance
+ .addProduct(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Product created successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm();
+ } else {
+ setNotif({
+ message: i18n.str`Could not create product`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not create product`,
+ message: i18n.str`Could not create product`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
index 580a92cdc..ca0b62704 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx
@@ -43,20 +43,20 @@ function createExample<Props>(
return r;
}
-export const Example = createExample(TestedComponent, {
- instances: [
- {
- id: "orderid",
- description: "description1",
- description_i18n: {} as any,
- image: "",
- price: "TESTKUDOS:10" as AmountString,
- taxes: [],
- total_lost: 10,
- total_sold: 5,
- total_stock: 15,
- unit: "bar",
- address: {},
- },
- ],
-});
+// export const Example = createExample(TestedComponent, {
+// instances: [
+// {
+// id: "orderid",
+// description: "description1",
+// description_i18n: {} as any,
+// image: "",
+// price: "TESTKUDOS:10" as AmountString,
+// taxes: [],
+// total_lost: 10,
+// total_sold: 5,
+// total_stock: 15,
+// unit: "bar",
+// address: {},
+// },
+// ],
+// });
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
index 39e2fd0c7..a4e3663ef 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -32,6 +32,7 @@ import {
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js";
+import { WithId } from "../../../../declaration.js";
type Entity = TalerMerchantApi.ProductDetail & WithId;
@@ -74,7 +75,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add product to inventory`}
+ data-tooltip={i18n.str`Add product to inventory`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -137,7 +138,7 @@ function Table({
<div class="table-container">
{onLoadMoreBefore && (
<button class="button is-fullwidth" onClick={onLoadMoreBefore}>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -226,7 +227,7 @@ function Table({
}
style={{ cursor: "pointer" }}
>
- {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`}
+ {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`}
</td>
<td
onClick={() =>
@@ -267,7 +268,7 @@ function Table({
<div class="buttons is-right">
<span
class="has-tooltip-bottom"
- data-tooltip={i18n.str`go to product update page`}
+ data-tooltip={i18n.str`Go to product update page`}
>
<button
class="button is-small is-success "
@@ -279,7 +280,7 @@ function Table({
</span>
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`remove this product from the database`}
+ data-tooltip={i18n.str`Remove this product from the database`}
>
<button
class="button is-small is-danger"
@@ -314,9 +315,9 @@ function Table({
</table>
{onLoadMoreAfter && (
<button class="button is-fullwidth"
- data-tooltip={i18n.str`load more products after the last one`}
+ data-tooltip={i18n.str`Load more products after the last one`}
onClick={onLoadMoreAfter}>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
@@ -357,7 +358,7 @@ function FastProductWithInfiniteStockUpdateForm({
<InputCurrency<FastProductUpdate>
name="price"
label={i18n.str`Price`}
- tooltip={i18n.str`update the product with new price`}
+ tooltip={i18n.str`Update the product with new price`}
/>
</FormProvider>
@@ -369,7 +370,7 @@ function FastProductWithInfiniteStockUpdateForm({
</button>
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`update product with new price`}
+ data-tooltip={i18n.str`Update product with new price`}
>
<button
class="button is-info"
@@ -406,7 +407,7 @@ function FastProductWithManagedStockUpdateForm({
const errors: FormErrors<FastProductUpdate> = {
lost:
currentStock + value.incoming < value.lost
- ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ ? `lost can't be greater that current + incoming (max ${currentStock + value.incoming
})`
: undefined,
};
@@ -427,17 +428,17 @@ function FastProductWithManagedStockUpdateForm({
<InputNumber<FastProductUpdate>
name="incoming"
label={i18n.str`Incoming`}
- tooltip={i18n.str`add more elements to the inventory`}
+ 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`}
+ 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`}
+ tooltip={i18n.str`New price for the product`}
/>
</FormProvider>
@@ -449,8 +450,8 @@ function FastProductWithManagedStockUpdateForm({
class="has-tooltip-left"
data-tooltip={
hasErrors
- ? i18n.str`the are value with errors`
- : i18n.str`update product with new stock and price`
+ ? i18n.str`The are value with errors`
+ : i18n.str`Update product with new stock and price`
}
>
<button
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
index 6ad0d4598..8755e7338 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -19,10 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -31,27 +34,23 @@ import { JumpToElementById } from "../../../../components/form/JumpToElementById
import { NotificationCard } from "../../../../components/menu/index.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
import { useSessionContext } from "../../../../context/session.js";
-import {
- useInstanceProducts
-} from "../../../../hooks/product.js";
+import { useInstanceProducts } from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CardTable } from "./Table.js";
+import { WithId } from "../../../../declaration.js";
interface Props {
onCreate: () => void;
onSelect: (id: string) => void;
}
-export default function ProductList({
- onCreate,
- onSelect,
-}: Props): VNode {
+export default function ProductList({ onCreate, onSelect }: Props): VNode {
const result = useInstanceProducts();
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
- const [deleting, setDeleting] =
- useState<TalerMerchantApi.ProductDetail & WithId | null>(null);
+ const { state, lib } = useSessionContext();
+ const [deleting, setDeleting] = useState<
+ (TalerMerchantApi.ProductDetail & WithId) | null
+ >(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -66,7 +65,7 @@ export default function ProductList({
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -84,8 +83,8 @@ export default function ProductList({
return resp.type === "ok";
}}
onSelect={onSelect}
- description={i18n.str`jump to product with the given product ID`}
- placeholder={i18n.str`product id`}
+ description={i18n.str`Jump to product with the given product ID`}
+ placeholder={i18n.str`Product id`}
/>
<CardTable
@@ -95,19 +94,31 @@ export default function ProductList({
onCreate={onCreate}
onUpdate={async (id, prod) => {
try {
- await lib.instance.updateProduct(state.token, id, prod);
- setNotif({
- message: i18n.str`product updated successfully`,
- type: "SUCCESS",
- });
+ const resp = await lib.instance.updateProduct(
+ state.token,
+ id,
+ prod,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Product updated successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not update the product`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
} catch (error) {
setNotif({
- message: i18n.str`could not update the product`,
+ message: i18n.str`Could not update the product`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
});
}
- return
+ return;
}}
onSelect={(product) => onSelect(product.id)}
onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) =>
@@ -124,14 +135,25 @@ export default function ProductList({
onCancel={() => setDeleting(null)}
onConfirm={async (): Promise<void> => {
try {
- await lib.instance.deleteProduct(state.token, deleting.id);
- setNotif({
- message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
- type: "SUCCESS",
- });
+ const resp = await lib.instance.deleteProduct(
+ state.token,
+ deleting.id,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not delete the product`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
} catch (error) {
setNotif({
- message: i18n.str`Failed to delete product`,
+ message: i18n.str`Could not delete the product`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
});
@@ -140,11 +162,17 @@ export default function ProductList({
}}
>
<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
+ <i18n.Translate>
+ 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
+ </i18n.Translate>
</p>
<p class="warning">
- Deleting an product <b>cannot be undone</b>.
+ <i18n.Translate>
+ Deleting an product can't be undone.
+ </i18n.Translate>
</p>
</ConfirmModal>
)}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
index 7aa93b186..889ea1b26 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx
@@ -41,34 +41,34 @@ function createExample<Props>(
return r;
}
-export const WithManagedStock = createExample(TestedComponent, {
- product: {
- product_id: "20102-ASDAS-QWE",
- description: "description1",
- description_i18n: {} as any,
- image: "",
- price: "TESTKUDOS:10" as AmountString,
- taxes: [],
- total_lost: 10,
- total_sold: 5,
- total_stock: 15,
- unit: "bar",
- address: {},
- },
-});
+// export const WithManagedStock = createExample(TestedComponent, {
+// product: {
+// product_id: "20102-ASDAS-QWE",
+// description: "description1",
+// description_i18n: {} as any,
+// image: "",
+// price: "TESTKUDOS:10" as AmountString,
+// 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" as AmountString,
- taxes: [],
- total_lost: 10,
- total_sold: 5,
- total_stock: -1,
- unit: "bar",
- address: {},
- },
-});
+// export const WithInfiniteStock = createExample(TestedComponent, {
+// product: {
+// product_id: "20102-ASDAS-QWE",
+// description: "description1",
+// description_i18n: {} as any,
+// image: "",
+// price: "TESTKUDOS:10" as AmountString,
+// taxes: [],
+// total_lost: 10,
+// total_sold: 5,
+// total_stock: -1,
+// unit: "bar",
+// address: {},
+// },
+// });
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
index 5e3e58d80..56626cfa8 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -48,9 +48,8 @@ export default function UpdateProduct({
}: Props): VNode {
const result = useProductDetails(pid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
-
+ const { state, lib } = useSessionContext();
+
const { i18n } = useTranslationContext();
if (!result) return <Loading />;
@@ -71,6 +70,7 @@ export default function UpdateProduct({
}
}
+
return (
<Fragment>
<NotificationCard notification={notif} />
@@ -79,12 +79,27 @@ export default function UpdateProduct({
onBack={onBack}
onUpdate={(data) => {
return lib.instance.updateProduct(state.token, pid, data)
- .then(onConfirm)
+ .then(resp => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Product (ID: ${pid}) has been updated`,
+ type: "SUCCESS",
+ });
+ onConfirm()
+ } else {
+ setNotif({
+ message: i18n.str`Could not update product`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not create product`,
+ message: i18n.str`Could not update product`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
index 50262be17..c20f8edf0 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -27,9 +27,7 @@ import {
TalerMerchantApi,
TranslatedString,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+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";
@@ -69,8 +67,7 @@ interface Props {
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { config } = useSessionContext();
- const { state: session } = useSessionContext();
+ const { config, state: session } = useSessionContext();
const devices = useInstanceOtpDevices();
const [state, setState] = useState<Partial<Entity>>({
@@ -94,28 +91,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const errors: FormErrors<Entity> = {
id: !state.id
- ? i18n.str`should not be empty`
+ ? i18n.str`Required`
: !/[a-zA-Z0-9]*/.test(state.id)
- ? i18n.str`no valid. only characters and numbers`
+ ? i18n.str`Invalid. only characters and numbers`
: undefined,
- description: !state.description ? i18n.str`should not be empty` : undefined,
+ description: !state.description ? i18n.str`Required` : undefined,
amount: !state.amount
- ? state.amount_editable ? undefined : i18n.str`required`
+ ? state.amount_editable
+ ? undefined
+ : i18n.str`Required`
: !parsedPrice
- ? i18n.str`not valid`
+ ? i18n.str`Invalid`
: Amounts.isZero(parsedPrice)
- ? state.amount_editable ? undefined : i18n.str`must be greater than 0`
+ ? state.amount_editable
+ ? undefined
+ : i18n.str`Must be greater than 0`
: undefined,
minimum_age:
state.minimum_age && state.minimum_age < 0
- ? i18n.str`should be greater that 0`
+ ? i18n.str`Must be greater that 0`
: undefined,
pay_duration: !state.pay_duration
- ? i18n.str`can't be empty`
+ ? i18n.str`Required`
: state.pay_duration.d_ms === "forever"
? undefined
: state.pay_duration.d_ms < 1000 //less than one second
- ? i18n.str`to short`
+ ? i18n.str`To short`
: undefined,
};
@@ -125,12 +126,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
(k) => (errors as Record<string, unknown>)[k] !== undefined,
);
- const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency))
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency));
const submitForm = () => {
if (hasErrors) return Promise.reject();
- const contract_amount = state.amount_editable ? undefined : state.amount as AmountString
- const contract_summary = state.summary_editable ? undefined : state.summary
+ const contract_amount = state.amount_editable
+ ? undefined
+ : (state.amount as AmountString);
+ const contract_summary = state.summary_editable ? undefined : state.summary;
const template_contract: TalerMerchantApi.TemplateContractDetails = {
minimum_age: state.minimum_age!,
pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
@@ -140,15 +143,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
cList.length > 1 && state.currency_editable
? undefined
: config.currency,
- }
+ };
return onCreate({
template_id: state.id!,
template_description: state.description!,
template_contract,
- required_currency: contract_amount !== undefined ? undefined : config.currency,
editable_defaults: {
- amount: !state.amount_editable ? undefined : (state.amount ?? zero),
- summary: !state.summary_editable ? undefined : (state.summary ?? ""),
+ amount: !state.amount_editable ? undefined : state.amount ?? zero,
+ summary: !state.summary_editable ? undefined : state.summary ?? "",
currency:
cList.length === 1 || !state.currency_editable
? undefined
@@ -182,7 +184,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
<InputWithAddon<Entity>
name="id"
help={
- new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href
+ new URL(
+ `templates/${state.id ?? ""}`,
+ session.backendUrl.href,
+ ).href
}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
@@ -225,7 +230,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
tooltip={i18n.str`Allow the user to change currency.`}
/>
<TextField name="sc" label={i18n.str`Supported currencies`}>
- <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate>
+ <i18n.Translate>
+ Supported currencies: {cList.join(", ")}
+ </i18n.Translate>
</TextField>
</Fragment>
)}
@@ -282,7 +289,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
index 499c7c859..bc615c8b8 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -28,15 +28,13 @@ import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-export type Entity = TalerMerchantApi.TransferInformation;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
-export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+export default function CreateTemplate({ onConfirm, onBack }: Props): VNode {
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -45,14 +43,29 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: TalerMerchantApi.TemplateAddDetails) => {
- return lib.instance.addTemplate(state.token, request)
- .then(() => onConfirm())
+ onCreate={async (request: TalerMerchantApi.TemplateAddDetails) => {
+ return lib.instance
+ .addTemplate(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Template has been created`,
+ type: "SUCCESS",
+ });
+ onConfirm();
+ } else {
+ setNotif({
+ message: i18n.str`Could not create template`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not inform template`,
+ message: i18n.str`Could not create template`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
deleted file mode 100644
index 66d8a2f7e..000000000
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- 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 { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { CardTable } from "./Table.js";
-
-export interface Props {
- templates: TalerMerchantApi.TemplateEntry[];
- onLoadMoreBefore?: () => void;
- onLoadMoreAfter?: () => void;
- onCreate: () => void;
- onDelete: (e: TalerMerchantApi.TemplateEntry) => void;
- onSelect: (e: TalerMerchantApi.TemplateEntry) => void;
- onNewOrder: (e: TalerMerchantApi.TemplateEntry) => void;
- onQR: (e: TalerMerchantApi.TemplateEntry) => void;
-}
-
-export function ListPage({
- templates,
- onCreate,
- onDelete,
- onSelect,
- onNewOrder,
- onQR,
- onLoadMoreBefore,
- onLoadMoreAfter,
-}: Props): VNode {
-
- return (
- <CardTable
- templates={templates.map((o) => ({
- ...o,
- id: String(o.template_id),
- }))}
- onQR={onQR}
- onCreate={onCreate}
- onDelete={onDelete}
- onSelect={onSelect}
- onNewOrder={onNewOrder}
- onLoadMoreBefore={onLoadMoreBefore}
- onLoadMoreAfter={onLoadMoreAfter}
- />
- );
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
index 4c55bae2a..80c893049 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -63,7 +63,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new templates`}
+ data-tooltip={i18n.str`Add new templates`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -124,10 +124,10 @@ function Table({
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more templates before the first one`}
+ data-tooltip={i18n.str`Load more templates before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -162,21 +162,21 @@ function Table({
<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`}
+ data-tooltip={i18n.str`Delete selected templates from the database`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</button>
<button
class="button is-info is-small has-tooltip-left"
- data-tooltip={i18n.str`use template to create new order`}
+ data-tooltip={i18n.str`Use template to create new order`}
onClick={() => onNewOrder(i)}
>
- Use template
+ <i18n.Translate>Use template</i18n.Translate>
</button>
<button
class="button is-info is-small has-tooltip-left"
- data-tooltip={i18n.str`create qr code for the template`}
+ data-tooltip={i18n.str`Create qr code for the template`}
onClick={() => onQR(i)}
>
QR
@@ -191,10 +191,10 @@ function Table({
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more templates after the last one`}
+ data-tooltip={i18n.str`Load more templates after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
index fce14dcc3..6c79c47ff 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -19,10 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -31,13 +34,11 @@ import { JumpToElementById } from "../../../../components/form/JumpToElementById
import { NotificationCard } from "../../../../components/menu/index.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
import { useSessionContext } from "../../../../context/session.js";
-import {
- useInstanceTemplates
-} from "../../../../hooks/templates.js";
+import { useInstanceTemplates } from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-import { ListPage } from "./ListPage.js";
+import { CardTable } from "./Table.js";
interface Props {
onCreate: () => void;
@@ -54,26 +55,25 @@ export default function ListTemplates({
}: Props): VNode {
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useInstanceTemplates();
const [deleting, setDeleting] =
useState<TalerMerchantApi.TemplateEntry | null>(null);
- const { state } = useSessionContext();
- if (!result) return <Loading />
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.NotFound: {
- return <NotFoundPageOrAdminCreate />
+ return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -84,25 +84,21 @@ export default function ListTemplates({
<JumpToElementById
testIfExist={async (id) => {
- const resp = await lib.instance.getTemplateDetails(state.token, id)
- return resp.type === "ok"
+ const resp = await lib.instance.getTemplateDetails(state.token, id);
+ return resp.type === "ok";
}}
onSelect={onSelect}
- description={i18n.str`jump to template with the given template ID`}
- placeholder={i18n.str`template id`}
+ description={i18n.str`Jump to template with the given template ID`}
+ placeholder={i18n.str`Template identification`}
/>
- <ListPage
- // templates={result.body}
- // onLoadMoreBefore={
- // result.isFirstPage ? undefined: result.loadFirst
- // }
- // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
-
- templates={result.body.templates}
+ <CardTable
+ templates={result.body.templates.map((o) => ({
+ ...o,
+ id: String(o.template_id),
+ }))}
onLoadMoreBefore={undefined}
onLoadMoreAfter={undefined}
-
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.template_id);
@@ -114,9 +110,8 @@ export default function ListTemplates({
onQR(e.template_id);
}}
onDelete={(e: TalerMerchantApi.TemplateEntry) => {
- setDeleting(e)
- }
- }
+ setDeleting(e);
+ }}
/>
{deleting && (
@@ -128,11 +123,22 @@ export default function ListTemplates({
onCancel={() => setDeleting(null)}
onConfirm={async (): Promise<void> => {
try {
- await lib.instance.deleteTemplate(state.token, deleting.template_id);
- setNotif({
- message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
- type: "SUCCESS",
- });
+ const resp = await lib.instance.deleteTemplate(
+ state.token,
+ deleting.template_id,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Failed to delete template`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
} catch (error) {
setNotif({
message: i18n.str`Failed to delete template`,
@@ -144,11 +150,18 @@ export default function ListTemplates({
}}
>
<p>
- If you delete the template <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
- <b>{deleting.template_id}</b>) you may loose information
+ <i18n.Translate>
+ If you delete the template{" "}
+ <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
+ <b>{deleting.template_id}</b>) you may loose information
+ </i18n.Translate>
</p>
<p class="warning">
- Deleting an template <b>cannot be undone</b>.
+ <i18n.Translate>Deleting an template </i18n.Translate>
+ <b>
+ <i18n.Translate>can't be undone</i18n.Translate>
+ </b>
+ .
</p>
</ConfirmModal>
)}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
index 547996ea1..ec3bec184 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -21,11 +21,9 @@
import {
TalerMerchantApi,
- stringifyPayTemplateUri
+ stringifyPayTemplateUri,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { QR } from "../../../../components/exception/QR.js";
import { useSessionContext } from "../../../../context/session.js";
@@ -42,29 +40,6 @@ export function QrPage({ id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const { state } = useSessionContext();
- // 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 = state.backendUrl.href;
const payTemplateUri = stringifyPayTemplateUri({
@@ -79,7 +54,9 @@ export function QrPage({ id: templateId, onBack }: Props): VNode {
<section id="printThis">
<QR text={payTemplateUri} />
<pre style={{ textAlign: "center" }}>
- <a target="_blank" rel="noreferrer" href={payTemplateUri}>{payTemplateUri}</a>
+ <a target="_blank" rel="noreferrer" href={payTemplateUri}>
+ {payTemplateUri}
+ </a>
</pre>
</section>
@@ -87,35 +64,6 @@ export function QrPage({ id: templateId, onBack }: Props): VNode {
<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={i18n.str`Amount`}
- readonly
- tooltip={i18n.str`Amount of the order`}
- />
- <Input<Entity>
- name="summary"
- inputType="multiline"
- readonly
- label={i18n.str`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}>
@@ -138,18 +86,31 @@ export function QrPage({ id: templateId, onBack }: Props): VNode {
}
function saveAsPDF(name: string): void {
- const printWindow = window.open("", "", "height=400,width=800");
- if (!printWindow) return;
+ // TODO: Look into using media queries in the current page, to print the current page, instead of opening a new window
+
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));
+
+ let dom = `<!DOCTYPE html>
+<html>
+ <head>
+ <title>Order template for ${name}</title>
+ <style>
+ pre > a {
+ text-decoration: none;
+ }
+ </style>
+ </head>
+ <body>
+ ${divContents.outerHTML}
+ </body>
+</html>`;
+ const blobUrl = URL.createObjectURL(new Blob([dom]));
+ const printWindow = window.open(blobUrl, "", "height=400,width=800");
+ if (!printWindow) return;
printWindow.addEventListener("load", () => {
printWindow.print();
- // printWindow.close();
+ printWindow.close();
+ URL.revokeObjectURL(blobUrl);
});
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
index ed809c7b3..93347f616 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -19,13 +19,16 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
-import {
- useTemplateDetails
-} from "../../../../hooks/templates.js";
+import { useTemplateDetails } from "../../../../hooks/templates.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { QrPage } from "./QrPage.js";
import { LoginPage } from "../../../login/index.js";
@@ -36,31 +39,27 @@ interface Props {
tid: string;
}
-export default function TemplateQrPage({
- tid,
- onBack,
-}: Props): VNode {
+export default function TemplateQrPage({ tid, onBack }: Props): VNode {
const result = useTemplateDetails(tid);
- if (!result) return <Loading />
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.NotFound: {
- return <NotFoundPageOrAdminCreate />
+ return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
-
return (
- <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} />
+ <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} />
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
index 32c5637aa..37f2bf898 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -46,6 +46,7 @@ import { InputToggle } from "../../../../components/form/InputToggle.js";
import { TextField } from "../../../../components/form/TextField.js";
import { useSessionContext } from "../../../../context/session.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { WithId } from "../../../../declaration.js";
type Entity = {
description?: string;
@@ -67,8 +68,7 @@ interface Props {
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { config } = useSessionContext();
- const { state: session } = useSessionContext();
+ const { config, state: session } = useSessionContext();
const [state, setState] = useState<Partial<Entity>>({
description: template.template_description,
@@ -115,24 +115,24 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
const errors: FormErrors<Entity> = {
- description: !state.description ? i18n.str`should not be empty` : undefined,
+ description: !state.description ? i18n.str`Required` : undefined,
amount: !state.amount
- ? state.amount_editable ? undefined : i18n.str`required`
+ ? state.amount_editable ? undefined : i18n.str`Required`
: !parsedPrice
- ? i18n.str`not valid`
+ ? i18n.str`Invalid`
: Amounts.isZero(parsedPrice)
- ? state.amount_editable ? undefined : i18n.str`must be greater than 0`
+ ? state.amount_editable ? undefined : i18n.str`Must be greater than 0`
: undefined,
minimum_age:
state.minimum_age && state.minimum_age < 0
- ? i18n.str`should be greater that 0`
+ ? i18n.str`Must be greater that 0`
: undefined,
pay_duration: !state.pay_duration
- ? i18n.str`can't be empty`
+ ? i18n.str`Required`
: state.pay_duration.d_ms === "forever"
? undefined
: state.pay_duration.d_ms < 1000 // less than one second
- ? i18n.str`to short`
+ ? i18n.str`Too short`
: undefined,
};
@@ -161,7 +161,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
return onUpdate({
template_description: state.description!,
template_contract,
- required_currency: contract_amount !== undefined ? undefined : config.currency,
editable_defaults: {
amount: !state.amount_editable ? undefined : (state.amount ?? zero),
summary: !state.summary_editable ? undefined : (state.summary ?? ""),
@@ -237,7 +236,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
/>
<TextField name="sc" label={i18n.str`Supported currencies`}>
<i18n.Translate>
- supported currencies: {cList.join(", ")}
+ Supported currencies: {cList.join(", ")}
</i18n.Translate>
</TextField>
</Fragment>
@@ -295,7 +294,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
index 6185bd2a9..5e3608334 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -19,23 +19,25 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { useSessionContext } from "../../../../context/session.js";
-import {
- useTemplateDetails,
-} from "../../../../hooks/templates.js";
+import { useTemplateDetails } from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
+import { WithId } from "../../../../declaration.js";
export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId;
@@ -49,27 +51,26 @@ export default function UpdateTemplate({
onConfirm,
onBack,
}: Props): VNode {
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useTemplateDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (!result) return <Loading />
+ if (!result) return <Loading />;
if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />
+ return <ErrorLoadingMerchant error={result} />;
}
if (result.type === "fail") {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.NotFound: {
- return <NotFoundPageOrAdminCreate />
+ return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -78,16 +79,31 @@ export default function UpdateTemplate({
<Fragment>
<NotificationCard notification={notif} />
<UpdatePage
- template={{...result.body, id: tid}}
+ template={{ ...result.body, id: tid }}
onBack={onBack}
onUpdate={(data) => {
- return lib.instance.updateTemplate(state.token, tid, data)
- .then(onConfirm)
+ return lib.instance
+ .updateTemplate(state.token, tid, data)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Template (ID: ${tid}) has been updated`,
+ type: "SUCCESS",
+ });
+ onConfirm();
+ } else {
+ setNotif({
+ message: i18n.str`Could not update template`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not update template`,
+ message: i18n.str`Could not update template`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
index 00cb2b827..504932ecb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -82,23 +82,49 @@ export default function TemplateUsePage({
onCreateOrder={(
request: TalerMerchantApi.UsingTemplateDetails,
) => {
-
return lib.instance.useTemplateCreateOrder(tid, request)
- .then((res) => {
- if (res.type === "ok") {
- onOrderCreated(res.body.order_id)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ onOrderCreated(resp.body.order_id)
} else {
- setNotif({
- message: i18n.str`could not create order from template`,
- type: "ERROR",
- });
+ switch (resp.case) {
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: i18n.str`No exchange would accept a payment because of KYC requirements.`
+ });
+ return;
+ }
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ return;
+ }
+ case HttpStatusCode.Gone: {
+ setNotif({
+ message: i18n.str`Could not create order`,
+ type: "ERROR",
+ description: i18n.str`No more stock for product with id "${resp.body.product_id}".`
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp)
+ }
+ }
}
})
.catch((error) => {
setNotif({
- message: i18n.str`could not create order from template`,
+ message: i18n.str`Could not create order from template`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
index d718ffb69..be23299ff 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -28,6 +28,7 @@ import { Input } from "../../../components/form/Input.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { useSessionContext } from "../../../context/session.js";
import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util";
+import { undefinedIfEmpty } from "../../../utils/table.js";
interface Props {
hasToken: boolean | undefined;
@@ -50,25 +51,23 @@ export function DetailPage({
});
const { i18n } = useTranslationContext();
- const errors = {
+ const errors = undefinedIfEmpty({
old_token:
hasToken && !form.old_token
- ? i18n.str`you need your access token to perform the operation`
+ ? i18n.str`You need your access token to perform the operation`
: undefined,
new_token: !form.new_token
- ? i18n.str`cannot be empty`
+ ? i18n.str`Required`
: form.new_token === form.old_token
- ? i18n.str`cannot be the same as the old token`
+ ? i18n.str`Can't be the same as the old token`
: undefined,
repeat_token:
form.new_token !== form.repeat_token
- ? i18n.str`is not the same`
+ ? i18n.str`Is not the same`
: undefined,
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as Record<string, unknown>)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const { state } = useSessionContext();
@@ -120,7 +119,7 @@ export function DetailPage({
<Input<State>
name="old_token"
label={i18n.str`Current access token`}
- tooltip={i18n.str`access token currently in use`}
+ tooltip={i18n.str`Access token currently in use`}
inputType="password"
/>
<p>
@@ -149,13 +148,13 @@ export function DetailPage({
<Input<State>
name="new_token"
label={i18n.str`New access token`}
- tooltip={i18n.str`next access token to be used`}
+ 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`}
+ tooltip={i18n.str`Confirm the same access token`}
inputType="password"
/>
</Fragment>
@@ -171,7 +170,7 @@ export function DetailPage({
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
index c23e5be17..b9659951c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -18,9 +18,7 @@ import {
TalerError,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
@@ -40,8 +38,7 @@ interface Props {
export default function Token({ onChange, onCancel }: Props): VNode {
const { i18n } = useTranslationContext();
- const { lib } = useSessionContext();
- const { logIn } = useSessionContext();
+ const { logIn, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const result = useInstanceDetails();
@@ -85,17 +82,15 @@ export default function Token({ onChange, onCancel }: Props): VNode {
return setNotif({
message: i18n.str`Failed to clear token`,
type: "ERROR",
- description: resp.detail.hint,
+ description: resp.detail?.hint,
});
}
} catch (error) {
- if (error instanceof Error) {
- return setNotif({
- message: i18n.str`Failed to clear token`,
- type: "ERROR",
- description: error.message,
- });
- }
+ return setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
}
}}
onNewToken={async (currentToken, newToken): Promise<void> => {
@@ -113,7 +108,7 @@ export default function Token({ onChange, onCancel }: Props): VNode {
return setNotif({
message: i18n.str`Failed to set new token`,
type: "ERROR",
- description: resp.detail.hint,
+ description: resp.detail?.hint,
});
}
}
@@ -137,13 +132,11 @@ export default function Token({ onChange, onCancel }: Props): VNode {
});
}
} catch (error) {
- if (error instanceof Error) {
return setNotif({
message: i18n.str`Failed to set new token`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
- }
}
}}
/>
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx
index 5542c028a..82038c918 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,14 +16,14 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
import { h, VNode, FunctionalComponent } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
export default {
- title: "Pages/Reserve/Create",
+ title: "Pages/TokenFamily/Create",
component: TestedComponent,
argTypes: {
onCreate: { action: "onCreate" },
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx
index becaf8f3a..cab5ba9cf 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,19 +16,17 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
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";
+import { TokenFamilyForm } from "../../../../components/tokenfamily/TokenFamilyForm.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Products.ProductAddDetail & {
- product_id: string;
-};
+type Entity = TalerMerchantApi.TokenFamilyCreateRequest;
interface Props {
onCreate: (d: Entity) => Promise<void>;
@@ -51,7 +49,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
- <ProductForm onSubscribe={addFormSubmitter} />
+ <TokenFamilyForm onSubscribe={addFormSubmitter} />
+
+ {/* <Test /> */}
<div class="buttons is-right mt-5">
{onBack && (
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx
index 648846793..1850d4638 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,55 +16,59 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
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 { useSessionContext } from "../../../../context/session.js";
import { CreatePage } from "./CreatePage.js";
-import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
-import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
-export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
+export type Entity = TalerMerchantApi.TokenFamilyCreateRequest;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
-
-export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { createOtpDevice } = useOtpDeviceAPI();
+export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode {
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} />
- }
+ const { state, lib } = useSessionContext();
return (
- <>
+ <Fragment>
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: Entity) => {
- return createOtpDevice(request)
- .then((d) => {
- setCreated(request)
+ onCreate={(request) => {
+ return lib.instance.createTokenFamily(state.token, request)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Token familty created successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm();
+ } else {
+ setNotif({
+ message: i18n.str`Could not create token family`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
})
.catch((error) => {
setNotif({
- message: i18n.str`could not create device`,
+ message: i18n.str`Could not create token family`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
/>
- </>
+ </Fragment>
);
}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx
index 2c97b59e8..28823e8a1 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -16,33 +16,24 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
-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 { 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";
+import { format } from "date-fns";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
-type Entity = AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId;
+type Entity = TalerMerchantApi.TokenFamilySummary;
interface Props {
instances: Entity[];
- onDelete: (id: Entity) => void;
- onSelect: (depositConfirmation: Entity) => void;
+ onDelete: (tokenFamily: Entity) => void;
+ onSelect: (tokenFamily: Entity) => void;
onUpdate: (
- id: string,
- data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ slug: string,
+ data: TalerMerchantApi.TokenFamilyUpdateRequest,
) => Promise<void>;
onCreate: () => void;
selected?: boolean;
@@ -66,12 +57,12 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-shopping" />
</span>
- <i18n.Translate>Deposit Confirmations</i18n.Translate>
+ <i18n.Translate>Token Families</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add deposit-confirmation`}
+ data-tooltip={i18n.str`Add token family`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -105,12 +96,12 @@ export function CardTable({
interface TableProps {
rowSelection: string | undefined;
instances: Entity[];
- onSelect: (id: Entity) => void;
+ onSelect: (tokenFamily: Entity) => void;
onUpdate: (
- id: string,
- data: AuditorBackend.DepositConfirmation.DepositConfirmationDetail,
+ slug: string,
+ data: TalerMerchantApi.TokenFamilyUpdateRequest,
) => Promise<void>;
- onDelete: (serial_id: Entity) => void;
+ onDelete: (tokenFamily: Entity) => void;
rowSelectionHandler: StateUpdater<string | undefined>;
}
@@ -119,59 +110,86 @@ function Table({
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>
+ <i18n.Translate>Slug</i18n.Translate>
</th>
<th>
- <i18n.Translate>Price per unit</i18n.Translate>
+ <i18n.Translate>Name</i18n.Translate>
</th>
<th>
- <i18n.Translate>Taxes</i18n.Translate>
+ <i18n.Translate>Valid After</i18n.Translate>
</th>
<th>
- <i18n.Translate>Sales</i18n.Translate>
+ <i18n.Translate>Valid Before</i18n.Translate>
</th>
<th>
- <i18n.Translate>Stock</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Sold</i18n.Translate>
+ <i18n.Translate>Kind</i18n.Translate>
</th>
<th />
</tr>
</thead>
<tbody>
{instances.map((i) => {
-
return (
- <Fragment key={i.id}>
+ <Fragment key={i.slug}>
<tr key="info">
<td
onClick={() =>
- rowSelection !== i.id && rowSelectionHandler(i.id)
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
}
style={{ cursor: "pointer" }}
>
+ {i.slug}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.name}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.valid_after.t_s === "never"
+ ? "never"
+ : format(new Date(i.valid_after.t_s * 1000), "yyyy/MM/dd hh:mm:ss")}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.valid_before.t_s === "never"
+ ? "never"
+ : format(new Date(i.valid_before.t_s * 1000), "yyyy/MM/dd hh:mm:ss")}
+ </td>
+ <td
+ onClick={() =>
+ rowSelection !== i.slug && rowSelectionHandler(i.slug)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.kind}
</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`}
+ data-tooltip={i18n.str`Go to token family update page`}
>
<button
class="button is-small is-success "
@@ -183,7 +201,7 @@ function Table({
</span>
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`remove this product from the database`}
+ data-tooltip={i18n.str`Remove this token family from the database`}
>
<button
class="button is-small is-danger"
@@ -196,12 +214,6 @@ function Table({
</div>
</td>
</tr>
- {rowSelection === i.id && (
- <tr key="form">
- <td colSpan={10}>
- </td>
- </tr>
- )}
</Fragment>
);
})}
@@ -211,16 +223,6 @@ function Table({
);
}
-interface FastProductUpdate {
- incoming: number;
- lost: number;
- price: string;
-}
-interface UpdatePrice {
- price: string;
-}
-
-
function EmptyTable(): VNode {
const { i18n } = useTranslationContext();
@@ -233,17 +235,9 @@ function EmptyTable(): VNode {
</p>
<p>
<i18n.Translate>
- There is no products yet, add more pressing the + sign
+ There are no token families yet, add the first one by 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(":");
-} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx
new file mode 100644
index 000000000..75da1496c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx
@@ -0,0 +1,169 @@
+/*
+ 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 Christian Blättler
+ */
+
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceTokenFamilies } from "../../../../hooks/tokenfamily.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { CardTable } from "./Table.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (slug: string) => void;
+}
+export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode {
+ const result = useInstanceTokenFamilies();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib, state } = useSessionContext();
+ const [deleting, setDeleting] =
+ useState<TalerMerchantApi.TokenFamilySummary | null>(null);
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+
+ <CardTable
+ instances={result.body.token_families}
+ onCreate={onCreate}
+ onUpdate={async (slug, fam) => {
+ try {
+ const resp = await lib.instance.updateTokenFamily(
+ state.token,
+ slug,
+ fam,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Token family updated successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not update the token family`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Could not update the token family`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ return;
+ }}
+ onSelect={(tokenFamily) => onSelect(tokenFamily.slug)}
+ onDelete={(fam) => setDeleting(fam)}
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete token family`}
+ description={`Delete the token family "${deleting.name}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ const resp = await lib.instance.deleteTokenFamily(
+ state.token,
+ deleting.slug,
+ );
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Failed to delete token family`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete token family`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ <i18n.Translate>
+ If you delete the <b>&quot;{deleting.name}&quot;</b> token family
+ (Slug: <b>{deleting.slug}</b>), all issued tokens will become
+ invalid.
+ </i18n.Translate>
+ </p>
+ <p class="warning">
+ <i18n.Translate>
+ Deleting a token family{" "}
+ <b>
+ <i18n.Translate>can't be undone</i18n.Translate>
+ </b>
+ .
+ </i18n.Translate>
+ </p>
+ </ConfirmModal>
+ )}
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx
new file mode 100644
index 000000000..382821f8c
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx
@@ -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/>
+ */
+
+/**
+ *
+ * @author Christian Blättler
+ */
+
+import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { 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 { InputDate } from "../../../../components/form/InputDate.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+
+type Entity = Omit<TalerMerchantApi.TokenFamilyUpdateRequest, "duration"> & {
+ duration: Duration,
+};
+
+interface Props {
+ onUpdate: (d: TalerMerchantApi.TokenFamilyUpdateRequest) => Promise<void>;
+ onBack?: () => void;
+ tokenFamily: TalerMerchantApi.TokenFamilyUpdateRequest;
+}
+
+function convert(from: TalerMerchantApi.TokenFamilyUpdateRequest) {
+ const { duration, ...rest } = from;
+
+ const converted = {
+ duration: Duration.fromTalerProtocolDuration(duration),
+ };
+ return { ...converted, ...rest };
+}
+
+export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) {
+ const [value, valueHandler] = useState<Partial<Entity>>(convert(tokenFamily));
+ const { i18n } = useTranslationContext();
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
+ name: !value.name ? i18n.str`Required` : undefined,
+ description: !value.description ? i18n.str`Required` : undefined,
+ valid_after: !value.valid_after ? i18n.str`Required` : undefined,
+ valid_before: !value.valid_before ? i18n.str`Required` : undefined,
+ duration: !value.duration ? i18n.str`Required` : undefined,
+ });
+
+ const hasErrors = errors !== undefined;
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+
+ const { duration, ...rest } = value as Required<Entity>;
+ const result: TalerMerchantApi.TokenFamilyUpdateRequest = {
+ ...rest,
+ duration: Duration.toTalerProtocolDuration(duration),
+ };
+
+ return onUpdate(result);
+ }
+
+
+ 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>Token Family: <b>{tokenFamily.name}</b></i18n.Translate>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider<Entity>
+ name="token_family"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <Input<Entity>
+ name="name"
+ inputType="text"
+ label={i18n.str`Name`}
+ tooltip={i18n.str`User-readable token family name`}
+ />
+ <Input<Entity>
+ name="description"
+ inputType="multiline"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`Token family description for customers`}
+ />
+ <InputDate<Entity>
+ name="valid_after"
+ label={i18n.str`Valid After`}
+ tooltip={i18n.str`Token family can issue tokens after this date`}
+ withTimestampSupport
+ />
+ <InputDate<Entity>
+ name="valid_before"
+ label={i18n.str`Valid Before`}
+ tooltip={i18n.str`Token family can issue tokens until this date`}
+ withTimestampSupport
+ />
+ <InputDuration<Entity>
+ name="duration"
+ label={i18n.str`Duration`}
+ tooltip={i18n.str`Validity duration of a issued token`}
+ withForever
+ />
+ </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`
+ : i18n.str`Confirm operation`
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx
new file mode 100644
index 000000000..92b015d13
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx
@@ -0,0 +1,123 @@
+/*
+ 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 Christian Blättler
+ */
+
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} 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 { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { Loading } from "../../../../components/exception/loading.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useTokenFamilyDetails } from "../../../../hooks/tokenfamily.js";
+import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+type Entity = TalerMerchantApi.TokenFamilyUpdateRequest;
+
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+ slug: string;
+}
+export default function UpdateTokenFamily({
+ slug,
+ onConfirm,
+ onBack,
+}: Props): VNode {
+ const result = useTokenFamilyDetails(slug);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib, state } = useSessionContext();
+
+ const { i18n } = useTranslationContext();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ const family: Entity = {
+ name: result.body.name,
+ description: result.body.description,
+ description_i18n: result.body.description_i18n || {},
+ duration: result.body.duration,
+ valid_after: result.body.valid_after,
+ valid_before: result.body.valid_before,
+ };
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ tokenFamily={family}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return lib.instance
+ .updateTokenFamily(state.token, slug, data)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Token familty updated successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm();
+ } else {
+ setNotif({
+ message: i18n.str`Could not update token family`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`Could not update token family`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : String(error),
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
index 91aabe58e..d3a1d7832 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -35,6 +35,7 @@ import {
CROCKFORD_BASE32_REGEX,
URL_REGEX,
} from "../../../../utils/constants.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = TalerMerchantApi.TransferInformation;
@@ -54,26 +55,24 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
credit_amount: `` as AmountString,
});
- const errors: FormErrors<Entity> = {
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
wtid: !state.wtid
- ? i18n.str`cannot be empty`
+ ? i18n.str`Required`
: !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,
+ ? i18n.str`Check the id, does not look valid`
+ : state.wtid.length !== 52
+ ? i18n.str`Must have 52 characters, current ${state.wtid.length}`
+ : undefined,
+ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined,
+ credit_amount: !state.credit_amount ? i18n.str`Required` : undefined,
exchange_url: !state.exchange_url
- ? i18n.str`cannot be empty`
+ ? i18n.str`Required`
: !URL_REGEX.test(state.exchange_url)
- ? i18n.str`URL doesn't have the right format`
- : undefined,
- };
+ ? i18n.str`URL doesn't have the right format`
+ : undefined,
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const submitForm = () => {
if (hasErrors) return Promise.reject();
@@ -102,7 +101,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
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`}
+ tooltip={i18n.str`Unique identifier of the wire transfer used by the exchange, must be 52 characters long`}
/>
<Input<Entity>
name="exchange_url"
@@ -128,7 +127,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
index 428476337..2ee03ead3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -38,8 +38,7 @@ interface Props {
}
export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const instance = useInstanceBankAccounts();
@@ -57,12 +56,26 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
onCreate={(request: TalerMerchantApi.TransferInformation) => {
return lib.instance
.informWireTransfer(state.token, request)
- .then(() => onConfirm())
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Wire transfer informed successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm()
+ } else {
+ setNotif({
+ message: i18n.str`Could not inform transfer`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not inform transfer`,
+ message: i18n.str`Could not inform transfer`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
index 22ad0b8d8..f80c0f53f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -19,12 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
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 { CardTable } from "./Table.js";
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
export interface Props {
transfers: TalerMerchantApi.TransferDetails[];
@@ -40,7 +40,7 @@ export interface Props {
onChangePayTo: (p?: string) => void;
payTo?: string;
onCreate: () => void;
- onDelete: () => void;
+ onDelete: (wid: TalerMerchantApi.TransferDetails) => void;
}
export function ListPage({
@@ -73,15 +73,15 @@ export function ListPage({
>
<InputSelector
name="payto_uri"
- label={i18n.str`Account URI`}
+ label={i18n.str`Bank account`}
values={accounts}
fromStr={(d) => {
const idx = accounts.indexOf(d)
if (idx === -1) return undefined;
return d
}}
- placeholder={i18n.str`Select one account`}
- tooltip={i18n.str`filter by account address`}
+ placeholder={i18n.str`All accounts`}
+ tooltip={i18n.str`Filter by account address`}
/>
</FormProvider>
</div>
@@ -92,7 +92,7 @@ export function ListPage({
<li class={isAllTransfers ? "is-active" : ""}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`remove all filters`}
+ data-tooltip={i18n.str`Remove all filters`}
>
<a onClick={onShowAll}>
<i18n.Translate>All</i18n.Translate>
@@ -102,7 +102,7 @@ export function ListPage({
<li class={isVerifiedTransfers ? "is-active" : ""}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`}
+ data-tooltip={i18n.str`Only show wire transfers confirmed by the merchant`}
>
<a onClick={onShowVerified}>
<i18n.Translate>Verified</i18n.Translate>
@@ -112,7 +112,7 @@ export function ListPage({
<li class={isNonVerifiedTransfers ? "is-active" : ""}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`only show wire transfers claimed by the exchange`}
+ data-tooltip={i18n.str`Only show wire transfers claimed by the exchange`}
>
<a onClick={onShowUnverified}>
<i18n.Translate>Unverified</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
index b9235c669..29ff69f55 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -25,6 +25,7 @@ import { format } from "date-fns";
import { h, VNode } from "preact";
import { StateUpdater, useState } from "preact/hooks";
import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js";
+import { WithId } from "../../../../declaration.js";
type Entity = TalerMerchantApi.TransferDetails & WithId;
@@ -60,7 +61,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new transfer`}
+ data-tooltip={i18n.str`Add new transfer`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -113,10 +114,10 @@ function Table({
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more transfers before the first one`}
+ data-tooltip={i18n.str`Load more transfers before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -129,12 +130,6 @@ function Table({
<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>
@@ -150,10 +145,8 @@ function Table({
{instances.map((i) => {
return (
<tr key={i.id}>
- <td>{i.id}</td>
+ <td title={i.wtid}>{i.wtid.substring(0,16)}...</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>
@@ -167,13 +160,13 @@ function Table({
: i18n.str`unknown`}
</td>
<td>
- {i.verified === undefined ? (
+ {i.verified !== true ? (
<button
class="button is-danger is-small has-tooltip-left"
- data-tooltip={i18n.str`delete selected transfer from the database`}
+ data-tooltip={i18n.str`Delete selected transfer from the database`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</button>
) : undefined}
</td>
@@ -185,10 +178,10 @@ function Table({
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more transfers after the last one`}
+ data-tooltip={i18n.str`Load more transfers after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
index 8b4d1f3cb..cae1a7fe3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -19,8 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
@@ -29,6 +33,10 @@ import { useInstanceTransfers } from "../../../../hooks/transfer.js";
import { LoginPage } from "../../../login/index.js";
import { ListPage } from "./ListPage.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { NotificationCard } from "../../../../components/menu/index.js";
+import { Notification } from "../../../../utils/types.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
onCreate: () => void;
@@ -38,25 +46,28 @@ interface Form {
payto_uri?: string;
}
-export default function ListTransfer({
- onCreate,
-}: Props): VNode {
+export default function ListTransfer({ onCreate }: Props): VNode {
const setFilter = (s?: boolean) => setForm({ ...form, verified: s });
+ const { i18n } = useTranslationContext();
+
+ const { state, lib } = useSessionContext();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
const [position, setPosition] = useState<string | undefined>(undefined);
const instance = useInstanceBankAccounts();
- const accounts = !instance || (instance instanceof TalerError) || instance.type === "fail"
- ? []
- : instance.body.accounts.map((a) => a.payto_uri);
+ const accounts =
+ !instance || instance instanceof TalerError || instance.type === "fail"
+ ? []
+ : instance.body.accounts.map((a) => a.payto_uri);
const [form, setForm] = useState<Form>({ payto_uri: "" });
- const shoulUseDefaultAccount = accounts.length === 1
+ const shoulUseDefaultAccount = accounts.length === 1;
useEffect(() => {
if (shoulUseDefaultAccount) {
- setForm({...form, payto_uri: accounts[0]})
+ setForm({ ...form, payto_uri: accounts[0] });
}
- }, [shoulUseDefaultAccount])
+ }, [shoulUseDefaultAccount]);
const isVerifiedTransfers = form.verified === true;
const isNonVerifiedTransfers = form.verified === false;
@@ -78,7 +89,7 @@ export default function ListTransfer({
if (result.type === "fail") {
switch (result.case) {
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
case HttpStatusCode.NotFound: {
return <NotFoundPageOrAdminCreate />;
@@ -90,23 +101,47 @@ export default function ListTransfer({
}
return (
- <ListPage
- accounts={accounts}
- transfers={result.body}
- onLoadMoreBefore={result.isFirstPage ? undefined: result.loadFirst }
- onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
- onCreate={onCreate}
- onDelete={() => {
- null;
- }}
- onShowAll={() => setFilter(undefined)}
- onShowUnverified={() => setFilter(false)}
- onShowVerified={() => setFilter(true)}
- isAllTransfers={isAllTransfers}
- isVerifiedTransfers={isVerifiedTransfers}
- isNonVerifiedTransfers={isNonVerifiedTransfers}
- payTo={form.payto_uri}
- onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))}
- />
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ accounts={accounts}
+ transfers={result.body}
+ onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
+ onCreate={onCreate}
+ onDelete={async (transfer) => {
+ try {
+ const resp = await lib.instance.deleteWireTransfer(state.token, String(transfer.transfer_serial_id));
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Wire transfer "${transfer.wtid.substring(0,16)}..." has been deleted`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Failed to delete transfer`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete transfer`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ }}
+ onShowAll={() => setFilter(undefined)}
+ onShowUnverified={() => setFilter(false)}
+ onShowVerified={() => setFilter(true)}
+ isAllTransfers={isAllTransfers}
+ isVerifiedTransfers={isVerifiedTransfers}
+ isNonVerifiedTransfers={isNonVerifiedTransfers}
+ payTo={form.payto_uri}
+ onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))}
+ />
+ </Fragment>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
index cde58967f..c7d38cf32 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -37,7 +37,6 @@ export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage,
default_wire_transfer_delay: Duration,
};
-//TalerMerchantApi.InstanceAuthConfigurationMessage
interface Props {
onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void;
selected: TalerMerchantApi.QueryInstancesResponse;
@@ -69,40 +68,38 @@ export function UpdatePage({
const { i18n } = useTranslationContext();
- const errors: FormErrors<Entity> = {
- name: !value.name ? i18n.str`required` : undefined,
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
+ name: !value.name ? i18n.str`Required` : undefined,
user_type: !value.user_type
- ? i18n.str`required`
+ ? i18n.str`Required`
: value.user_type !== "business" && value.user_type !== "individual"
- ? i18n.str`should be business or individual`
+ ? i18n.str`Must be business or individual`
: undefined,
default_pay_delay: !value.default_pay_delay
- ? i18n.str`required`
+ ? 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,
+ 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`
+ ? i18n.str`Required`
: undefined,
address: undefinedIfEmpty({
address_lines:
value.address?.address_lines && value.address?.address_lines.length > 7
- ? i18n.str`max 7 lines`
+ ? 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`
+ ? i18n.str`Max 7 lines`
: undefined,
}),
- };
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const submit = async (): Promise<void> => {
const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>;
@@ -111,7 +108,7 @@ export function UpdatePage({
default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay),
...rest,
}
- await onUpdate(result);
+ onUpdate(result);
};
// const [active, setActive] = useState(false);
@@ -159,7 +156,7 @@ export function UpdatePage({
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
disabled={hasErrors}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
index 9da7f7efb..2b2327eb2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -103,11 +103,11 @@ function CommonUpdate(
}
return updateInstance(state.token, d)
.then(onConfirm)
- .catch((error: Error) =>
+ .catch((error) =>
setNotif({
message: i18n.str`Failed to update instance`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
}),
);
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
index 8792aabea..234295174 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -30,6 +30,7 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = TalerMerchantApi.WebhookAddDetails;
@@ -45,26 +46,26 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
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"`
+ const errors = undefinedIfEmpty<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`Must be "pay" or "refund"`
: undefined,
http_method: !state.http_method
- ? i18n.str`required`
+ ? i18n.str`Required`
: !validMethod.includes(state.http_method)
- ? i18n.str`should be one of '${validMethod.join(", ")}'`
+ ? i18n.str`Must be one of '${validMethod.join(", ")}'`
: undefined,
- url: !state.url ? i18n.str`required` : undefined,
- };
+ url: !state.url ? i18n.str`Required` : undefined,
+ });
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
+ const hasErrors = errors !== undefined;
const submitForm = () => {
if (hasErrors) return Promise.reject();
- return onCreate(state as any);
+ return onCreate(state as TalerMerchantApi.WebhookAddDetails);
};
return (
@@ -88,8 +89,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
label={i18n.str`Event`}
values={[
i18n.str`Choose one...`,
- i18n.str`pay`,
- i18n.str`refund`,
+ i18n.str`Pay`,
+ i18n.str`Refund`,
]}
tooltip={i18n.str`The event of the webhook: why the webhook is used`}
/>
@@ -114,28 +115,79 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
/>
<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 corresponding variable.
+ {/* prettier will add some nodes which we don't want because of i18n */}
+ {/* prettier-ignore */}
+ <i18n.Translate>
+ 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
+ corresponding variable.
+ </i18n.Translate>
</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
+ {/* prettier will add some nodes which we don't want because of i18n */}
+ {/* prettier-ignore */}
+ <i18n.Translate>
+ 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
+ </i18n.Translate>
</p>
<p>
- The short list of variables are:
+ <i18n.Translate>
+ The short list of variables are:
+ </i18n.Translate>
</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
+ class="menu-list"
+ style={{ listStyleType: "disc", marginLeft: 20 }}
+ >
+ <li>
+ <b>contract_terms.summary:</b>{" "}
+ <i18n.Translate>order's description</i18n.Translate>
+ </li>
+ <li>
+ <b>contract_terms.amount:</b>{" "}
+ <i18n.Translate>order's price</i18n.Translate>
+ </li>
+ <li>
+ <b>order_id:</b>{" "}
+ <i18n.Translate>
+ order's unique identification
+ </i18n.Translate>
+ </li>
+ {state.event_type === "refund" && (
+ <Fragment>
+ <li>
+ <b>refund_amout:</b>{" "}
+ <i18n.Translate>
+ the amount that was being refunded
+ </i18n.Translate>
+ </li>
+ <li>
+ <b>reason:</b>{" "}
+ <i18n.Translate>
+ the reason entered by the merchant staff for granting
+ the refund
+ </i18n.Translate>
+ </li>
+ <li>
+ <b>timestamp:</b>{" "}
+ <i18n.Translate>
+ time of the refund in nanoseconds since 1970
+ </i18n.Translate>
+ </li>
+ </Fragment>
+ )}
</ul>
</div>
{/* <Input<Entity>
@@ -163,7 +215,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
index 70f246ff1..128124dc8 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -37,22 +37,35 @@ interface Props {
export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
return (
<>
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: TalerMerchantApi.WebhookAddDetails) => {
+ onCreate={async (request: TalerMerchantApi.WebhookAddDetails) => {
return lib.instance.addWebhook(state.token, request)
- .then(() => onConfirm())
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Webhook create successfully`,
+ type: "SUCCESS",
+ });
+ onConfirm()
+ } else {
+ setNotif({
+ message: i18n.str`Could not create the webhook`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not inform template`,
+ message: i18n.str`Could not create webhook`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
index 877bd30e5..2d42ea6a5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -59,7 +59,7 @@ export function CardTable({
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
- data-tooltip={i18n.str`add new webhooks`}
+ data-tooltip={i18n.str`Add new webhooks`}
>
<button class="button is-info" type="button" onClick={onCreate}>
<span class="icon is-small">
@@ -114,10 +114,10 @@ function Table({
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more webhooks before the first one`}
+ data-tooltip={i18n.str`Load more webhooks before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load first page</i18n.Translate>
+ <i18n.Translate>Load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -152,18 +152,11 @@ function Table({
<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`}
+ data-tooltip={i18n.str`Delete selected webhook from the database`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</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>
@@ -174,10 +167,10 @@ function Table({
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more webhooks after the last one`}
+ data-tooltip={i18n.str`Load more webhooks after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load next page</i18n.Translate>
+ <i18n.Translate>Load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
index 789b8d73b..fe374496f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -25,9 +25,7 @@ import {
TalerMerchantApi,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
@@ -48,8 +46,7 @@ interface Props {
export default function ListWebhooks({ onCreate, onSelect }: Props): VNode {
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useInstanceWebhooks();
if (!result) return <Loading />;
@@ -62,7 +59,7 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode {
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -85,17 +82,25 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode {
onDelete={(e: TalerMerchantApi.WebhookEntry) => {
return lib.instance
.deleteWebhook(state.token, e.webhook_id)
- .then(() =>
- setNotif({
- message: i18n.str`webhook delete successfully`,
- type: "SUCCESS",
- }),
- )
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Webhook delete successfully`,
+ type: "SUCCESS",
+ });
+ } else {
+ setNotif({
+ message: i18n.str`Could not delete the webhook`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+ }
+ })
.catch((error) =>
setNotif({
- message: i18n.str`could not delete the webhook`,
+ message: i18n.str`Could not delete the webhook`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
}),
);
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
index 6aca62582..1c1d0da79 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -19,6 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -28,7 +29,8 @@ import {
FormProvider,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { WithId } from "../../../../declaration.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = TalerMerchantApi.WebhookPatchDetails & WithId;
@@ -44,23 +46,21 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
const [state, setState] = useState<Partial<Entity>>(webhook);
- const errors: FormErrors<Entity> = {
- event_type: !state.event_type ? i18n.str`required` : undefined,
+ const errors = undefinedIfEmpty<FormErrors<Entity>>({
+ event_type: !state.event_type ? i18n.str`Required` : undefined,
http_method: !state.http_method
- ? i18n.str`required`
+ ? i18n.str`Required`
: !validMethod.includes(state.http_method)
- ? i18n.str`should be one of '${validMethod.join(", ")}'`
- : undefined,
- url: !state.url ? i18n.str`required` : undefined,
- };
+ ? i18n.str`Must 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 hasErrors = errors !== undefined;
const submitForm = () => {
if (hasErrors) return Promise.reject();
- return onUpdate(state as any);
+ return onUpdate(state as Entity);
};
return (
@@ -129,7 +129,7 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
- : "confirm operation"
+ : i18n.str`Confirm operation`
}
onClick={submitForm}
>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
index 5b2ba7bb9..82f1463f9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -29,6 +29,7 @@ import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchan
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { useSessionContext } from "../../../../context/session.js";
+import { WithId } from "../../../../declaration.js";
import {
useWebhookDetails,
} from "../../../../hooks/webhooks.js";
@@ -49,8 +50,7 @@ export default function UpdateWebhook({
onConfirm,
onBack,
}: Props): VNode {
- const { lib } = useSessionContext();
- const { state } = useSessionContext();
+ const { state, lib } = useSessionContext();
const result = useWebhookDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
@@ -80,14 +80,29 @@ export default function UpdateWebhook({
<UpdatePage
webhook={{ ...result.body, id: tid }}
onBack={onBack}
- onUpdate={(data) => {
+ onUpdate={async (data) => {
return lib.instance.updateWebhook(state.token, tid, data)
- .then(onConfirm)
+ .then((resp) => {
+ if (resp.type === "ok") {
+ setNotif({
+ message: i18n.str`Webhook updated`,
+ type: "SUCCESS",
+ });
+ onConfirm()
+ } else {
+ setNotif({
+ message: i18n.str`Could not update webhook`,
+ type: "ERROR",
+ description: resp.detail?.hint,
+ });
+
+ }
+ })
.catch((error) => {
setNotif({
- message: i18n.str`could not update template`,
+ message: i18n.str`Could not update webhook`,
type: "ERROR",
- description: error.message,
+ description: error instanceof Error ? error.message : String(error),
});
});
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index d77bc75fd..5b58c1690 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -19,10 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util";
import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+ HttpStatusCode,
+ createRFC8959AccessTokenEncoded,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../components/menu/index.js";
@@ -42,8 +43,7 @@ const tokenRequest = {
export function LoginPage(_p: Props): VNode {
const [token, setToken] = useState("");
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { state, logIn } = useSessionContext();
- const { lib } = useSessionContext();
+ const { lib, state, logIn } = useSessionContext();
const { i18n } = useTranslationContext();
@@ -60,14 +60,14 @@ export function LoginPage(_p: Props): VNode {
switch (result.case) {
case HttpStatusCode.Unauthorized: {
setNotif({
- message: "Your password is incorrect",
+ message: i18n.str`Your password is incorrect`,
type: "ERROR",
});
return;
}
case HttpStatusCode.NotFound: {
setNotif({
- message: "Your instance not found",
+ message: i18n.str`Your instance not found`,
type: "ERROR",
});
return;
diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
index 4d348c02b..e0ce17be8 100644
--- a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
@@ -31,7 +31,7 @@ import {
import InstanceCreatePage from "../../paths/admin/create/index.js";
import { InstancePaths } from "../../Routing.js";
-export function NotFoundPage(): VNode {
+function NotFoundPage(): VNode {
return (
<div>
<p>That page doesn&apos;t exist.</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 0c4b9dd1a..468bf2268 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { AbsoluteTime } from "@gnu-taler/taler-util";
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import {
@@ -24,7 +25,6 @@ import { InputSelector } from "../../components/form/InputSelector.js";
import { InputToggle } from "../../components/form/InputToggle.js";
import { LangSelector } from "../../components/menu/LangSelector.js";
import { Preferences, usePreference } from "../../hooks/preference.js";
-import { AbsoluteTime } from "@gnu-taler/taler-util";
function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined;
@@ -45,6 +45,8 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
const next = s(value);
const v: Preferences = {
advanceOrderMode: next.advanceOrderMode ?? false,
+ advanceInstanceMode: next.advanceInstanceMode ?? false,
+ developerMode: next.developerMode ?? false,
hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(),
hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(),
dateFormat: next.dateFormat ?? "ymd",
@@ -81,19 +83,6 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
</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));
- e.preventDefault()
- }}
- >
- <i18n.Translate>Set default</i18n.Translate>
- </button>
- )}
</div>
</div>
<InputToggle<Preferences>
@@ -101,6 +90,11 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
tooltip={i18n.str`Shows more options in the order creation form`}
name="advanceOrderMode"
/>
+ <InputToggle<Preferences>
+ label={i18n.str`Advance instance settings`}
+ tooltip={i18n.str`Shows more options in the instance settings form`}
+ name="advanceInstanceMode"
+ />
<InputSelector<Preferences>
name="dateFormat"
label={i18n.str`Date format`}
@@ -121,7 +115,12 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
return "choose one";
}}
values={["ymd", "mdy", "dmy"]}
- tooltip={i18n.str`how the date is going to be displayed`}
+ tooltip={i18n.str`How the date is going to be displayed`}
+ />
+ <InputToggle<Preferences>
+ label={i18n.str`Developer mode`}
+ tooltip={i18n.str`Shows more options and tools which are not intended for general audience.`}
+ name="developerMode"
/>
</FormProvider>
</div>
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
deleted file mode 100644
index 693894ae0..000000000
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- 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 { Amounts } from "@gnu-taler/taler-util";
-import { isAfter, isFuture } from "date-fns";
-import * as yup from "yup";
-import { PAYTO_REGEX } from "../utils/constants.js";
-
-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 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/merchant-backoffice-ui/src/scss/_aside.scss b/packages/merchant-backoffice-ui/src/scss/_aside.scss
index b7b59516b..719da7d2c 100644
--- a/packages/merchant-backoffice-ui/src/scss/_aside.scss
+++ b/packages/merchant-backoffice-ui/src/scss/_aside.scss
@@ -130,7 +130,11 @@ aside.aside {
@include touch {
nav.navbar {
- @include transition(margin-left);
+ // @include transition(margin-left);
+ // TODO: adapt above transition mixin to work with multiple transitions
+ transition:
+ margin-left 250ms ease-in-out 50ms,
+ width 250ms ease-in-out 50ms;
}
aside.aside {
@include transition(left);
@@ -173,6 +177,7 @@ aside.aside {
div.has-aside-mobile-expanded {
nav.navbar {
margin-left: $aside-mobile-width;
+ width: calc(100vw - $aside-mobile-width);
}
aside.aside {
left: 0;
diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss
index 6c7346eb3..c10b0cbd4 100644
--- a/packages/merchant-backoffice-ui/src/scss/toggle.scss
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -53,6 +53,22 @@ $green: #56c080;
left: 30px;
}
}
+ .toggle-checkbox:not(checked)+& {
+ background: $grey-lighter;
+
+ &:before {
+ left: 4px;
+ }
+ }
+ .toggle-checkbox:indeterminate+& {
+ background: rgba(0, 0, 0, 0.301);
+
+ &:before {
+ left: 16px;
+ background: rgba(0, 0, 0, 0.301);
+ }
+ }
+
}
.toggle-checkbox {
diff --git a/packages/merchant-backoffice-ui/src/settings.json b/packages/merchant-backoffice-ui/src/settings.json
new file mode 100644
index 000000000..bba9ed6cd
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "backendBaseURL": "http://merchant.taler.test:1180/"
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/type-override.d.ts
index 18e762642..703b60331 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/type-override.d.ts
@@ -15,14 +15,18 @@
*/
/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
+ * define unknown type of catch function
*/
-
-import { FunctionalComponent, h } from "preact";
-import { ListPage as TestedComponent } from "./ListPage.js";
-
-export default {
- title: "Pages/Accounts/List",
- component: TestedComponent,
-};
+interface Promise<T> {
+ /**
+ * Attaches a callback for only the rejection of the Promise.
+ * @param onrejected The callback to execute when the Promise is rejected.
+ * @returns A Promise for the completion of the callback.
+ */
+ catch<TResult = never>(
+ onrejected?:
+ | ((reason: unknown) => TResult | PromiseLike<TResult>)
+ | undefined
+ | null,
+ ): Promise<T | TResult>;
+}
diff --git a/packages/merchant-backoffice-ui/src/utils/table.ts b/packages/merchant-backoffice-ui/src/utils/table.ts
index 982b68e5e..af989c3e9 100644
--- a/packages/merchant-backoffice-ui/src/utils/table.ts
+++ b/packages/merchant-backoffice-ui/src/utils/table.ts
@@ -14,6 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { WithId } from "../declaration.js";
+
/**
*
@@ -49,7 +51,7 @@ export function buildActions<T extends WithId>(
* @returns
*/
export function undefinedIfEmpty<
- T extends Record<string, unknown> | Array<unknown>,
+ T extends object | 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/pogen/package.json b/packages/pogen/package.json
index 81d66125f..db11f1d1f 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.11.4",
+ "version": "0.13.4",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index a891cc7ba..2282931bb 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,135 @@
+taler-harness (0.13.4) unstable; urgency=low
+
+ * Release 0.13.4
+
+ -- Florian Dold <dold@taler.net> Thu, 19 Sep 2024 14:02:11 +0200
+
+taler-harness (0.13.3) unstable; urgency=low
+
+ * Release 0.13.3
+
+ -- Florian Dold <dold@taler.net> Tue, 17 Sep 2024 19:03:34 +0200
+
+taler-harness (0.13.2) unstable; urgency=low
+
+ * Release 0.13.2
+
+ -- Florian Dold <dold@taler.net> Wed, 11 Sep 2024 15:56:08 +0200
+
+taler-harness (0.13.1) unstable; urgency=low
+
+ * Release 0.13.1
+
+ -- Florian Dold <dold@taler.net> Wed, 28 Aug 2024 23:42:37 +0200
+
+taler-harness (0.13.0) unstable; urgency=low
+
+ * Release 0.13.0
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 20:55:35 +0200
+
+taler-harness (0.12.14~dev.1) unstable; urgency=low
+
+ * Release 0.12.14-dev.1
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 00:15:09 +0200
+
+taler-harness (0.12.14-dev.1) unstable; urgency=low
+
+ * Release 0.12.14-dev.1
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 00:11:56 +0200
+
+taler-harness (0.12.13) unstable; urgency=low
+
+ * Release 0.12.13
+
+ -- Florian Dold <dold@taler.net> Sun, 25 Aug 2024 14:21:08 +0200
+
+taler-harness (0.12.12) unstable; urgency=low
+
+ * Release 0.12.12
+
+ -- Florian Dold <dold@taler.net> Mon, 12 Aug 2024 22:45:00 -0300
+
+taler-harness (0.12.11) unstable; urgency=low
+
+ * Release 0.12.11
+
+ -- Florian Dold <dold@taler.net> Sun, 11 Aug 2024 22:31:39 +0200
+
+taler-harness (0.12.10) unstable; urgency=low
+
+ * Release 0.12.10
+
+ -- Florian Dold <dold@taler.net> Sun, 11 Aug 2024 17:17:11 +0200
+
+taler-harness (0.12.9) unstable; urgency=low
+
+ * Release 0.12.9
+
+ -- Florian Dold <dold@taler.net> Thu, 08 Aug 2024 16:51:49 +0200
+
+taler-harness (0.12.8) unstable; urgency=low
+
+ * Release 0.12.8
+
+ -- Florian Dold <dold@taler.net> Fri, 02 Aug 2024 11:37:02 -0600
+
+taler-harness (0.12.7) unstable; urgency=low
+
+ * Release 0.12.7
+
+ -- Florian Dold <dold@taler.net> Mon, 29 Jul 2024 10:57:50 +0200
+
+taler-harness (0.12.6) unstable; urgency=low
+
+ * Release 0.12.6
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Jul 2024 12:36:42 -0600
+
+taler-harness (0.12.5) unstable; urgency=low
+
+ * Release 0.12.5
+
+ -- Florian Dold <dold@taler.net> Wed, 17 Jul 2024 09:34:49 -0600
+
+taler-harness (0.12.4) unstable; urgency=low
+
+ * Release 0.12.4
+
+ -- Florian Dold <dold@taler.net> Mon, 15 Jul 2024 08:39:36 -0600
+
+taler-harness (0.12.3) unstable; urgency=low
+
+ * Release 0.12.3
+
+ -- Florian Dold <dold@taler.net> Mon, 15 Jul 2024 08:36:26 -0600
+
+taler-harness (0.12.2) unstable; urgency=low
+
+ * Release 0.12.2
+
+ -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200
+
+taler-harness (0.12.1) unstable; urgency=low
+
+ * Release 0.12.1
+
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600
+
+taler-harness (v0.12.1) unstable; urgency=low
+
+ * Release v0.12.1
+
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:20 -0600
+
+taler-harness (0.12.0) unstable; urgency=low
+
+ * Release 0.12.0
+
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 16:17:52 +0200
+
taler-harness (0.11.4) unstable; urgency=low
* Release 0.11.4
diff --git a/packages/taler-harness/gdb.txt b/packages/taler-harness/gdb.txt
new file mode 100644
index 000000000..d95ce235a
--- /dev/null
+++ b/packages/taler-harness/gdb.txt
@@ -0,0 +1,26 @@
+Program not restarted.
+Continuing.
+
+Program received signal SIGSEGV, Segmentation fault.
+0x00007fb0306a81dd in ?? () from /usr/lib/libc.so.6
+#0 0x00007fb0306a81dd in ?? () from /usr/lib/libc.so.6
+#1 0x00007fb03091865f in ?? () from /usr/lib/libmicrohttpd.so.12
+#2 0x00007fb030f150e0 in TALER_MHD_reply_legal (conn=0x55a945adfbb0, legal=0x0) at mhd_legal.c:362
+#3 0x000055a92f8b08f4 in proceed_with_handler (rc=rc@entry=0x55a945af8540, url=url@entry=0x55a945a9f15a "", upload_data=upload_data@entry=0x0, upload_data_size=upload_data_size@entry=0x7fff45b5fea0) at taler-exchange-httpd.c:1190
+#4 0x000055a92f8b1277 in handle_mhd_request (cls=<optimized out>, connection=0x55a945adfbb0, url=<optimized out>, method=0x55a945a9f150 "GET", version=<optimized out>, upload_data=0x0, upload_data_size=0x7fff45b5fea0, con_cls=0x55a945adfc68) at taler-exchange-httpd.c:1970
+#5 0x00007fb03091071f in ?? () from /usr/lib/libmicrohttpd.so.12
+#6 0x00007fb030913608 in ?? () from /usr/lib/libmicrohttpd.so.12
+#7 0x00007fb0309166d8 in ?? () from /usr/lib/libmicrohttpd.so.12
+#8 0x00007fb0309324e4 in ?? () from /usr/lib/libmicrohttpd.so.12
+#9 0x00007fb03090ff01 in ?? () from /usr/lib/libmicrohttpd.so.12
+#10 0x00007fb030918f7c in MHD_run () from /usr/lib/libmicrohttpd.so.12
+#11 0x00007fb030f19633 in run_daemon (cls=<optimized out>) at mhd_run.c:68
+#12 0x00007fb030e44b78 in GNUNET_SCHEDULER_do_work (sh=sh@entry=0x55a945a75850) at scheduler.c:2143
+#13 0x00007fb030e45a85 in select_loop (sh=0x55a945a75850, context=0x7fff45b60270) at scheduler.c:2442
+#14 GNUNET_SCHEDULER_run (task=task@entry=0x7fb030e3b630 <program_main>, task_cls=task_cls@entry=0x7fff45b60340) at scheduler.c:743
+#15 0x00007fb030e3bdde in GNUNET_PROGRAM_run2 (argc=<optimized out>, argc@entry=4, argv=argv@entry=0x7fff45b60918, binaryName=binaryName@entry=0x55a92f8ee36e "taler-exchange-httpd", binaryHelp=binaryHelp@entry=0x55a92f8ee352 "Taler exchange HTTP service", options=options@entry=0x7fff45b60550, task=task@entry=0x55a92f8b3480 <run>, task_cls=0x0, run_without_scheduler=0) at program.c:381
+#16 0x00007fb030e3c39f in GNUNET_PROGRAM_run (argc=argc@entry=4, argv=argv@entry=0x7fff45b60918, binaryName=binaryName@entry=0x55a92f8ee36e "taler-exchange-httpd", binaryHelp=binaryHelp@entry=0x55a92f8ee352 "Taler exchange HTTP service", options=options@entry=0x7fff45b60550, task=task@entry=0x55a92f8b3480 <run>, task_cls=0x0) at program.c:408
+#17 0x000055a92f8afc79 in main (argc=4, argv=0x7fff45b60918) at taler-exchange-httpd.c:2779
+Detaching from program: /home/avalos/.local/bin/taler-exchange-httpd, process 540834
+[Inferior 1 (process 540834) detached]
+The program is not being run.
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index bca870c8b..a40199343 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/bench2.ts b/packages/taler-harness/src/bench2.ts
index 90924caec..3caf28c1f 100644
--- a/packages/taler-harness/src/bench2.ts
+++ b/packages/taler-harness/src/bench2.ts
@@ -27,10 +27,8 @@ import {
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
- applyRunConfigDefaults,
CryptoDispatcher,
SynchronousCryptoWorkerFactoryPlain,
- Wallet,
} from "@gnu-taler/taler-wallet-core";
import {
checkReserve,
@@ -68,7 +66,7 @@ export async function runBench2(configJson: any): Promise<void> {
const reserveAmount = (numDeposits + 1) * 10;
- const defaultConfig = applyRunConfigDefaults();
+ const denomselAllowLate = false;
for (let i = 0; i < numIter; i++) {
const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http);
@@ -92,7 +90,7 @@ export async function runBench2(configJson: any): Promise<void> {
console.log("reserve found");
const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8` as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
});
for (let j = 0; j < numDeposits; j++) {
@@ -121,10 +119,10 @@ export async function runBench2(configJson: any): Promise<void> {
const refreshDenoms = [
findDenomOrThrow(exchangeInfo, `${curr}:1` as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
}),
findDenomOrThrow(exchangeInfo, `${curr}:1` as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
}),
];
diff --git a/packages/taler-harness/src/env-full.ts b/packages/taler-harness/src/env-full.ts
index bb2cb8c47..87ed200d5 100644
--- a/packages/taler-harness/src/env-full.ts
+++ b/packages/taler-harness/src/env-full.ts
@@ -25,7 +25,7 @@ import {
ExchangeService,
FakebankService,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
} from "./harness/harness.js";
/**
@@ -82,7 +82,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -91,7 +91,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 4fc462ddf..86ed98f1c 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -25,7 +25,6 @@
* Imports
*/
import {
- AccountRestriction,
AmountJson,
Amounts,
Configuration,
@@ -36,9 +35,12 @@ import {
MerchantInstanceConfig,
PartialMerchantInstanceConfig,
PaytoString,
+ TalerCoreBankHttpClient,
TalerCorebankApiClient,
TalerError,
+ TalerExchangeHttpClient,
TalerMerchantApi,
+ TalerMerchantManagementHttpClient,
WalletNotification,
createEddsaKeyPair,
eddsaGetPublic,
@@ -600,6 +602,12 @@ class BankServiceBase {
) {}
}
+export type RestrictionFlag = "credit-restriction" | "debit-restriction";
+
+export type HarnessAccountRestriction =
+ | [RestrictionFlag, "deny"]
+ | [RestrictionFlag, "regex", string, string, string];
+
export interface HarnessExchangeBankAccount {
accountName: string;
accountPassword: string;
@@ -608,13 +616,12 @@ export interface HarnessExchangeBankAccount {
conversionUrl?: string;
- debitRestrictions?: AccountRestriction[];
- creditRestrictions?: AccountRestriction[];
-
/**
* If set, the harness will not automatically configure the wire fee for this account.
*/
skipWireFeeCreation?: boolean;
+
+ accountRestrictions?: HarnessAccountRestriction[];
}
/**
@@ -710,7 +717,7 @@ export class FakebankService
return {
accountName: accountName,
accountPassword: password,
- accountPaytoUri: generateRandomPayto(accountName),
+ accountPaytoUri: getTestHarnessPaytoForLabel(accountName),
wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/accounts/${accountName}/taler-wire-gateway/`,
};
}
@@ -736,9 +743,20 @@ export class FakebankService
"bank",
);
await this.pingUntilAvailable();
- const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
- for (const acc of this.accounts) {
- await bankClient.registerAccount(acc.accountName, acc.accountPassword);
+ // Check version
+ {
+ const bankClient = new TalerCoreBankHttpClient(this.corebankApiBaseUrl);
+ // This would fail/throw if the version doesn't match.
+ const resp = await bankClient.getConfig();
+ this.globalTestState.assertTrue(resp.type === "ok");
+ }
+ // Register bank accounts
+ {
+ // FIXME: This is using the old bank client!
+ const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
+ for (const acc of this.accounts) {
+ await bankClient.registerAccount(acc.accountName, acc.accountPassword);
+ }
}
}
@@ -805,6 +823,7 @@ export class LibeufinBankService
"registration_bonus",
`${bc.currency}:100`,
);
+ config.setString("libeufin-bank", "ALLOW_REGISTRATION", "yes");
const cfgFilename = testDir + "/bank.conf";
config.writeTo(cfgFilename, { excludeDefaults: true });
@@ -890,9 +909,20 @@ export class LibeufinBankService
"libeufin-bank-httpd",
);
await this.pingUntilAvailable();
- const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
- for (const acc of this.accounts) {
- await bankClient.registerAccount(acc.accountName, acc.accountPassword);
+ // Check version
+ {
+ const bankClient = new TalerCoreBankHttpClient(this.corebankApiBaseUrl);
+ // This would fail/throw if the version doesn't match.
+ const resp = await bankClient.getConfig();
+ this.globalTestState.assertTrue(resp.type === "ok");
+ }
+ // Register accounts
+ {
+ // FIXME: This still uses the old-style client.
+ const bankClient = new TalerCorebankApiClient(this.corebankApiBaseUrl);
+ for (const acc of this.accounts) {
+ await bankClient.registerAccount(acc.accountName, acc.accountPassword);
+ }
}
}
@@ -1017,7 +1047,7 @@ export class ExchangeService implements ExchangeServiceInterface {
this.globalState,
`exchange-${this.name}-aggregator-once`,
"taler-exchange-aggregator",
- [...timetravelArgArr, "-c", this.configFilename, "-t", "-y", "-LINFO"],
+ [...timetravelArgArr, "-c", this.configFilename, "-t", "-LINFO"],
);
}
@@ -1026,14 +1056,7 @@ export class ExchangeService implements ExchangeServiceInterface {
this.globalState,
`exchange-${this.name}-aggregator-once`,
"taler-exchange-aggregator",
- [
- ...this.timetravelArgArr,
- "-c",
- this.configFilename,
- "-t",
- "-y",
- "-LINFO",
- ],
+ [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-LINFO"],
);
}
@@ -1361,6 +1384,9 @@ export class ExchangeService implements ExchangeServiceInterface {
if (acct.conversionUrl != null) {
optArgs.push("conversion-url", acct.conversionUrl);
}
+ if (acct.accountRestrictions != null) {
+ optArgs.push(...acct.accountRestrictions.flat(1));
+ }
await runCommand(
this.globalState,
@@ -1468,6 +1494,26 @@ export class ExchangeService implements ExchangeServiceInterface {
await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`);
}
+ /**
+ * Generate a new master public key for the exchange.
+ */
+ async regenerateMasterPub(): Promise<void> {
+ const cfg = Configuration.load(this.configFilename);
+ const masterPrivFile = cfg
+ .getPath("exchange-offline", "master_priv_file")
+ .required();
+ fs.unlinkSync(masterPrivFile);
+ const exchangeMasterKey = createEddsaKeyPair();
+ fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv));
+ cfg.setString(
+ "exchange",
+ "master_public_key",
+ encodeCrock(exchangeMasterKey.eddsaPub),
+ );
+
+ cfg.writeTo(this.configFilename, { excludeDefaults: true });
+ }
+
async purgeDatabase(): Promise<void> {
await sh(
this.globalState,
@@ -1556,6 +1602,13 @@ export class ExchangeService implements ExchangeServiceInterface {
await this.pingUntilAvailable();
+ {
+ const exchangeClient = new TalerExchangeHttpClient(this.baseUrl);
+ // Would throw on incompatible version.
+ const configResp = await exchangeClient.getConfig();
+ this.globalState.assertTrue(configResp.type === "ok");
+ }
+
const skipKeyup = opts.skipKeyup ?? false;
if (!skipKeyup) {
@@ -1565,6 +1618,26 @@ export class ExchangeService implements ExchangeServiceInterface {
}
}
+ async enableAmlAccount(
+ amlStaffPub: string,
+ legalName: string,
+ ): Promise<void> {
+ await runCommand(
+ this.globalState,
+ "exchange-offline",
+ "taler-exchange-offline",
+ [
+ "-c",
+ this.configFilename,
+ "aml-enable",
+ amlStaffPub,
+ legalName,
+ "rw",
+ "upload",
+ ],
+ );
+ }
+
async pingUntilAvailable(): Promise<void> {
// We request /management/keys, since /keys can block
// when we didn't do the key setup yet.
@@ -1612,7 +1685,9 @@ export class MerchantService implements MerchantServiceInterface {
return new MerchantService(gc, mc, cfgFilename);
}
- proc: ProcessWrapper | undefined;
+ procHttpd: ProcessWrapper | undefined;
+ procExchangekeyupdate: ProcessWrapper | undefined;
+ procKyccheck: ProcessWrapper | undefined;
constructor(
private globalState: GlobalTestState,
@@ -1623,7 +1698,7 @@ export class MerchantService implements MerchantServiceInterface {
private currentTimetravelOffsetMs: number | undefined;
private isRunning(): boolean {
- return !!this.proc;
+ return !!this.procHttpd;
}
setTimetravel(t: number | undefined): void {
@@ -1663,11 +1738,23 @@ export class MerchantService implements MerchantServiceInterface {
}
async stop(): Promise<void> {
- const httpd = this.proc;
+ const httpd = this.procHttpd;
if (httpd) {
httpd.proc.kill("SIGTERM");
await httpd.wait();
- this.proc = undefined;
+ this.procHttpd = undefined;
+ }
+ const exchangekeyupdate = this.procExchangekeyupdate;
+ if (exchangekeyupdate) {
+ exchangekeyupdate.proc.kill("SIGTERM");
+ await exchangekeyupdate.wait();
+ this.procExchangekeyupdate = undefined;
+ }
+ const kyccheck = this.procKyccheck;
+ if (kyccheck) {
+ kyccheck.proc.kill("SIGTERM");
+ await kyccheck.wait();
+ this.procKyccheck = undefined;
}
}
@@ -1680,6 +1767,19 @@ export class MerchantService implements MerchantServiceInterface {
);
}
+ async runReconciliationOnceWithTimetravel(opts: {
+ timetravelMicroseconds: number;
+ }) {
+ let timetravelArgArr = [];
+ timetravelArgArr.push(`--timetravel=${opts.timetravelMicroseconds}`);
+ await runCommand(
+ this.globalState,
+ `merchant-${this.name}-reconciliation-once`,
+ "taler-merchant-reconciliation",
+ [...timetravelArgArr, "-LINFO", "-c", this.configFilename, "-t"],
+ );
+ }
+
/**
* Start the merchant,
*/
@@ -1690,7 +1790,7 @@ export class MerchantService implements MerchantServiceInterface {
await this.dbinit();
}
- this.proc = this.globalState.spawnService(
+ this.procHttpd = this.globalState.spawnService(
"taler-merchant-httpd",
[
"taler-merchant-httpd",
@@ -1699,8 +1799,41 @@ export class MerchantService implements MerchantServiceInterface {
this.configFilename,
...this.timetravelArgArr,
],
- `merchant-${this.merchantConfig.name}`,
+ `merchant-httpd-${this.merchantConfig.name}`,
+ );
+
+ this.procExchangekeyupdate = this.globalState.spawnService(
+ "taler-merchant-exchangekeyupdate",
+ [
+ "taler-merchant-exchangekeyupdate",
+ "-LDEBUG",
+ "-c",
+ this.configFilename,
+ ...this.timetravelArgArr,
+ ],
+ `merchant-exchangekeyupdate-${this.merchantConfig.name}`,
+ );
+
+ this.procKyccheck = this.globalState.spawnService(
+ "taler-merchant-kyccheck",
+ [
+ "taler-merchant-kyccheck",
+ "-LDEBUG",
+ "-c",
+ this.configFilename,
+ ...this.timetravelArgArr,
+ ],
+ `merchant-kyccheck-${this.merchantConfig.name}`,
);
+
+ await this.pingUntilAvailable();
+ {
+ const merchantClient = new TalerMerchantManagementHttpClient(
+ this.makeInstanceBaseUrl(),
+ );
+ const configResp = await merchantClient.getConfig();
+ this.globalState.assertTrue(configResp.type === "ok");
+ }
}
static async create(
@@ -1748,7 +1881,7 @@ export class MerchantService implements MerchantServiceInterface {
return await this.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
@@ -1761,7 +1894,7 @@ export class MerchantService implements MerchantServiceInterface {
async addInstanceWithWireAccount(
instanceConfig: PartialMerchantInstanceConfig,
): Promise<void> {
- if (!this.proc) {
+ if (!this.procHttpd) {
throw Error("merchant must be running to add instance");
}
logger.info(`adding instance '${instanceConfig.id}'`);
@@ -1813,7 +1946,11 @@ export class MerchantService implements MerchantServiceInterface {
async pingUntilAvailable(): Promise<void> {
const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
- await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`);
+ await pingProc(
+ this.procHttpd,
+ url,
+ `merchant (${this.merchantConfig.name})`,
+ );
}
}
@@ -2255,15 +2392,7 @@ export function generateRandomTestIban(salt: string | null = null): string {
return `DE${check_digits}${bban}`;
}
-export function getWireMethodForTest(): string {
- return "x-taler-bank";
-}
-
-/**
- * Generate a payto address, whose authority depends
- * on whether the banking is served by euFin or Pybank.
- */
-export function generateRandomPayto(label: string): string {
+export function getTestHarnessPaytoForLabel(label: string): string {
return `payto://x-taler-bank/localhost/${label}?receiver-name=${label}`;
}
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index d194b0d36..73dcd1550 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -24,16 +24,24 @@
* Imports
*/
import {
+ AmlDecisionRequest,
+ AmlDecisionRequestWithoutSignature,
AmountString,
ConfirmPayResultType,
+ decodeCrock,
Duration,
+ encodeCrock,
+ HttpStatusCode,
+ LegitimizationRuleSet,
Logger,
MerchantApiClient,
NotificationType,
PartialWalletRunConfig,
PreparePayResultType,
+ signAmlDecision,
TalerCorebankApiClient,
TalerMerchantApi,
+ TalerProtocolTimestamp,
TransactionMajorState,
WalletNotification,
} from "@gnu-taler/taler-util";
@@ -49,19 +57,21 @@ import {
ExchangeService,
ExchangeServiceInterface,
FakebankService,
+ getTestHarnessPaytoForLabel,
GlobalTestState,
+ HarnessAccountRestriction,
HarnessExchangeBankAccount,
+ harnessHttpLib,
LibeufinBankService,
MerchantService,
MerchantServiceInterface,
+ setupDb,
+ setupSharedDb,
+ useLibeufinBank,
WalletCli,
WalletClient,
WalletService,
WithAuthorization,
- generateRandomPayto,
- setupDb,
- setupSharedDb,
- useLibeufinBank,
} from "./harness.js";
import * as fs from "fs";
@@ -100,6 +110,7 @@ export interface SimpleTestEnvironmentNg {
*/
export interface SimpleTestEnvironmentNg3 {
commonDb: DbInfo;
+ bank: BankService;
bankClient: TalerCorebankApiClient;
exchange: ExchangeService;
exchangeBankAccount: HarnessExchangeBankAccount;
@@ -118,6 +129,10 @@ export interface EnvOptions {
skipWireFeeCreation?: boolean;
+ walletTestObservability?: boolean;
+
+ accountRestrictions?: HarnessAccountRestriction[];
+
additionalExchangeConfig?(e: ExchangeService): void;
additionalMerchantConfig?(m: MerchantService): void;
additionalBankConfig?(b: BankService): void;
@@ -255,7 +270,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -264,7 +279,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -387,7 +402,7 @@ export async function createSimpleTestkudosEnvironmentV2(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -396,7 +411,7 @@ export async function createSimpleTestkudosEnvironmentV2(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -462,9 +477,9 @@ export async function createSimpleTestkudosEnvironmentV3(
const receiverName = "Exchange";
const exchangeBankUsername = "exchange";
const exchangeBankPassword = "mypw";
- const exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
const wireGatewayApiBaseUrl = new URL(
- "accounts/exchange/taler-wire-gateway/",
+ `accounts/${exchangeBankUsername}/taler-wire-gateway/`,
bank.corebankApiBaseUrl,
).href;
@@ -474,6 +489,7 @@ export async function createSimpleTestkudosEnvironmentV3(
accountPassword: exchangeBankPassword,
accountPaytoUri: exchangePaytoUri,
skipWireFeeCreation: opts.skipWireFeeCreation === true,
+ accountRestrictions: opts.accountRestrictions,
};
await exchange.addBankAccount("1", exchangeBankAccount);
@@ -528,7 +544,6 @@ export async function createSimpleTestkudosEnvironmentV3(
opts.additionalExchangeConfig(exchange);
}
await exchange.start();
- await exchange.pingUntilAvailable();
merchant.addExchange(exchange);
@@ -541,7 +556,7 @@ export async function createSimpleTestkudosEnvironmentV3(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -550,7 +565,7 @@ export async function createSimpleTestkudosEnvironmentV3(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -558,7 +573,11 @@ export async function createSimpleTestkudosEnvironmentV3(
const { walletClient, walletService } = await createWalletDaemonWithClient(
t,
- { name: "wallet", persistent: true },
+ {
+ name: "wallet",
+ persistent: true,
+ emitObservabilityEvents: !!opts.walletTestObservability,
+ },
);
console.log("setup done!");
@@ -569,6 +588,7 @@ export async function createSimpleTestkudosEnvironmentV3(
merchant,
walletClient,
walletService,
+ bank,
bankClient,
exchangeBankAccount,
};
@@ -580,6 +600,7 @@ export interface CreateWalletArgs {
persistent?: boolean;
overrideDbPath?: string;
config?: PartialWalletRunConfig;
+ emitObservabilityEvents?: boolean;
}
export async function createWalletDaemonWithClient(
@@ -617,7 +638,9 @@ export async function createWalletDaemonWithClient(
const defaultRunConfig = {
testing: {
skipDefaults: true,
- emitObservabilityEvents: !!process.env["TALER_TEST_OBSERVABILITY"],
+ emitObservabilityEvents:
+ !!process.env["TALER_TEST_OBSERVABILITY"] ||
+ !!args.emitObservabilityEvents,
},
} satisfies PartialWalletRunConfig;
await walletClient.client.call(WalletApiOperation.InitWallet, {
@@ -714,13 +737,13 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
@@ -742,7 +765,21 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
}
export interface WithdrawViaBankResult {
+ /**
+ * Payto URI of the account that the withdrawal
+ * originated from. Typically a new account used for testing.
+ */
+ accountPaytoUri: string;
+
+ /**
+ * Helper promise that resolves when withdrawal has finished successfully.
+ */
withdrawalFinishedCond: Promise<true>;
+
+ /**
+ * The wallet-core withdrawal transaction ID.
+ */
+ transactionId: string;
}
/**
@@ -799,7 +836,9 @@ export async function withdrawViaBankV2(
});
return {
+ accountPaytoUri: user.accountPaytoUri,
withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
};
}
@@ -863,7 +902,9 @@ export async function withdrawViaBankV3(
});
return {
+ accountPaytoUri: user.accountPaytoUri,
withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
};
}
@@ -955,3 +996,99 @@ export async function makeTestPaymentV2(
t.assertTrue(orderStatus.order_status === "paid");
}
+
+/**
+ * Post an AML decision that no rules shall apply for the given account.
+ */
+export async function postAmlDecisionNoRules(
+ t: GlobalTestState,
+ req: {
+ exchangeBaseUrl: string;
+ paytoHash: string;
+ amlPriv: string;
+ amlPub: string;
+ },
+) {
+ const { exchangeBaseUrl, paytoHash, amlPriv, amlPub } = req;
+
+ const sigData: AmlDecisionRequestWithoutSignature = {
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: paytoHash,
+ justification: "Bla",
+ keep_investigating: false,
+ new_rules: {
+ custom_measures: {},
+ expiration_time: TalerProtocolTimestamp.never(),
+ rules: [],
+ successor_measure: undefined,
+ },
+ properties: {
+ foo: "42",
+ },
+ };
+
+ const sig = signAmlDecision(decodeCrock(amlPriv), sigData);
+
+ const reqBody: AmlDecisionRequest = {
+ ...sigData,
+ officer_sig: encodeCrock(sig),
+ };
+
+ const reqUrl = new URL(`aml/${amlPub}/decision`, exchangeBaseUrl);
+
+ const resp = await harnessHttpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: reqBody,
+ });
+
+ console.log(`aml decision status: ${resp.status}`);
+
+ t.assertDeepEqual(resp.status, HttpStatusCode.NoContent);
+}
+
+/**
+ * Post an AML decision that no rules shall apply for the given account.
+ */
+export async function postAmlDecision(
+ t: GlobalTestState,
+ req: {
+ exchangeBaseUrl: string;
+ paytoHash: string;
+ amlPriv: string;
+ amlPub: string;
+ newRules: LegitimizationRuleSet;
+ newMeasure?: string | undefined;
+ },
+) {
+ const { exchangeBaseUrl, paytoHash, amlPriv, amlPub } = req;
+
+ const sigData: AmlDecisionRequestWithoutSignature = {
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: paytoHash,
+ justification: "Bla",
+ keep_investigating: false,
+ new_rules: req.newRules,
+ new_measures: req.newMeasure,
+ properties: {
+ foo: "42",
+ },
+ };
+
+ const sig = signAmlDecision(decodeCrock(amlPriv), sigData);
+
+ const reqBody: AmlDecisionRequest = {
+ ...sigData,
+ officer_sig: encodeCrock(sig),
+ };
+
+ const reqUrl = new URL(`aml/${amlPub}/decision`, exchangeBaseUrl);
+
+ const resp = await harnessHttpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: reqBody,
+ });
+
+ console.log(`aml decision status: ${resp.status}`);
+
+ t.assertDeepEqual(resp.status, HttpStatusCode.NoContent);
+}
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index 99b5502d8..53a103629 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -19,6 +19,7 @@
*/
import {
AccessToken,
+ AmountJson,
AmountString,
Amounts,
BalancesResponse,
@@ -302,8 +303,6 @@ advancedCli
const { walletClient, walletService, bank, exchange, merchant } =
await createSimpleTestkudosEnvironmentV2(t);
await walletClient.call(WalletApiOperation.RunIntegrationTestV2, {
- amountToSpend: "TESTKUDOS:5" as AmountString,
- amountToWithdraw: "TESTKUDOS:10" as AmountString,
corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
@@ -729,7 +728,7 @@ deploymentCli
const bc = await bank.getConfig();
if (bc.type === "fail") {
- logger.error(`couldn't get bank config. ${bc.detail.hint}`);
+ logger.error(`couldn't get bank config. ${bc.detail?.hint}`);
return;
}
if (!bank.isCompatible(bc.body.version)) {
@@ -740,7 +739,7 @@ deploymentCli
}
const mc = await merchantManager.getConfig();
if (mc.type === "fail") {
- logger.error(`couldn't get merchant config. ${mc.detail.hint}`);
+ logger.error(`couldn't get merchant config. ${mc.detail?.hint}`);
return;
}
if (!merchantManager.isCompatible(mc.body.version)) {
@@ -761,7 +760,7 @@ deploymentCli
"admin",
bankAdminPassword,
{
- scope: "write",
+ scope: "readwrite",
duration: {
d_us: 1000 * 1000 * 10, //10 secs
},
@@ -799,6 +798,7 @@ deploymentCli
logger.error(
`unable to provision bank account, HTTP response status ${resp.case}`,
);
+ logger.error(j2s(resp));
process.exit(2);
}
logger.info(`account ${id} successfully provisioned`);
@@ -1039,10 +1039,19 @@ deploymentCli
.maybeOption("bankURL", ["--bankURL"], clk.STRING)
.maybeOption("bankUser", ["--bankUser"], clk.STRING)
.maybeOption("bankPassword", ["--bankPassword"], clk.STRING)
+ .maybeOption(
+ "defaultWireTransferDelay",
+ ["--default-wire-transfer-delay"],
+ clk.STRING,
+ )
+ .maybeOption("defaultPayDelay", ["--default-pay-delay"], clk.STRING)
.action(async (args) => {
const httpLib = createPlatformHttpLib({});
const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl;
- const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib);
+ const managementApi = new TalerMerchantManagementHttpClient(
+ baseUrl,
+ httpLib,
+ );
const managementToken = createRFC8959AccessTokenEncoded(
args.provisionMerchantInstance.managementToken,
);
@@ -1059,16 +1068,34 @@ deploymentCli
const bankPassword = args.provisionMerchantInstance.bankPassword;
const accountPayto = args.provisionMerchantInstance.payto as PaytoString;
- const createResp = await api.createInstance(managementToken, {
+ let defaultWireTransferDelay: Duration;
+ if (args.provisionMerchantInstance.defaultWireTransferDelay) {
+ defaultWireTransferDelay = Duration.fromPrettyString(
+ args.provisionMerchantInstance.defaultWireTransferDelay,
+ );
+ } else {
+ defaultWireTransferDelay = Duration.fromMilliseconds(1);
+ }
+
+ let defaultPayDelay: Duration;
+ if (args.provisionMerchantInstance.defaultPayDelay) {
+ defaultPayDelay = Duration.fromPrettyString(
+ args.provisionMerchantInstance.defaultPayDelay,
+ );
+ } else {
+ defaultPayDelay = Duration.fromSpec({ hours: 1 });
+ }
+
+ const createResp = await managementApi.createInstance(managementToken, {
address: {},
auth: {
method: "token",
token: instanceTokenPlain,
},
- default_pay_delay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ hours: 1 }),
+ default_pay_delay: Duration.toTalerProtocolDuration(defaultPayDelay),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(
+ defaultWireTransferDelay,
),
- default_wire_transfer_delay: { d_us: 1 },
id: instanceId,
jurisdiction: {},
name: instancceName,
@@ -1086,18 +1113,28 @@ deploymentCli
process.exit(2);
}
- const createAccountResp = await api.addBankAccount(instanceTokenEnc, {
- payto_uri: accountPayto,
- credit_facade_url: bankURL,
- credit_facade_credentials:
- bankUser && bankPassword
- ? {
- type: "basic",
- username: bankUser,
- password: bankPassword,
- }
- : undefined,
- });
+ const instanceUrl = managementApi.getSubInstanceAPI(instanceId).href;
+
+ const instanceApi = new TalerMerchantInstanceHttpClient(
+ instanceUrl,
+ httpLib,
+ );
+
+ const createAccountResp = await instanceApi.addBankAccount(
+ instanceTokenEnc,
+ {
+ payto_uri: accountPayto,
+ credit_facade_url: bankURL,
+ credit_facade_credentials:
+ bankUser && bankPassword
+ ? {
+ type: "basic",
+ username: bankUser,
+ password: bankPassword,
+ }
+ : undefined,
+ },
+ );
if (createAccountResp.type != "ok") {
console.error(
`unable to configure bank account for instance ${instanceId}, status ${createAccountResp.case}`,
@@ -1141,9 +1178,27 @@ deploymentCli
logger.error(
`unable to provision bank account, HTTP response status ${resp.case}`,
);
+ logger.error(j2s(resp));
process.exit(2);
});
+function computeFee(args: {
+ currency: string;
+ spec: string;
+ coinMin: AmountJson;
+ coinMax: AmountJson;
+}): string {
+ if (args.spec === "none") {
+ return `${args.currency}:0`;
+ }
+
+ if (args.spec === "const") {
+ return Amounts.stringify(args.coinMin);
+ }
+
+ throw Error(`unsupported fee spec (${args.spec})`);
+}
+
deploymentCli
.subcommand("coincfg", "gen-coin-config", {
help: "Generate a coin/denomination configuration for the exchange.",
@@ -1154,40 +1209,89 @@ deploymentCli
.requiredOption("maxAmount", ["--max-amount"], clk.STRING, {
help: "Largest denomination",
})
+ .maybeOption("feeWithdraw", ["--fee-deposit"], clk.STRING)
+ .maybeOption("feeDeposit", ["--fee-deposit"], clk.STRING)
+ .maybeOption("feeRefresh", ["--fee-refresh"], clk.STRING)
+ .maybeOption("feeRefund", ["--fee-refund"], clk.STRING)
+ .maybeOption("fees", ["--fees"], clk.STRING)
.flag("noFees", ["--no-fees"])
.action(async (args) => {
+ let feespecWithdraw;
+ let feespecDeposit;
+ let feespecRefund;
+ let feespecRefresh;
+
+ if (args.coincfg.noFees) {
+ feespecDeposit = "none";
+ feespecWithdraw = "none";
+ feespecRefund = "none";
+ feespecRefresh = "none";
+ } else if (args.coincfg.fees) {
+ feespecWithdraw = args.coincfg.fees;
+ feespecDeposit = args.coincfg.fees;
+ feespecRefund = args.coincfg.fees;
+ feespecRefresh = args.coincfg.fees;
+ } else {
+ // Default: Only deposit fees
+ feespecWithdraw = args.coincfg.feeWithdraw ?? "none";
+ feespecDeposit = args.coincfg.feeDeposit ?? "const";
+ feespecRefund = args.coincfg.feeRefund ?? "none";
+ feespecRefresh = args.coincfg.feeRefresh ?? "none";
+ }
+
let out = "";
const stamp = Math.floor(new Date().getTime() / 1000);
- const min = Amounts.parseOrThrow(args.coincfg.minAmount);
- const max = Amounts.parseOrThrow(args.coincfg.maxAmount);
- if (min.currency != max.currency) {
+ const coinMin = Amounts.parseOrThrow(args.coincfg.minAmount);
+ const coinMax = Amounts.parseOrThrow(args.coincfg.maxAmount);
+ if (coinMin.currency != coinMax.currency) {
console.error("currency mismatch");
process.exit(1);
}
- const currency = min.currency;
- let x = min;
+ const currency = coinMin.currency;
+ let x = coinMin;
let n = 1;
out += "# Coin configuration for the exchange.\n";
out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n';
out += "\n";
- while (Amounts.cmp(x, max) < 0) {
+ while (Amounts.cmp(x, coinMax) < 0) {
+ const feeWithdraw = computeFee({
+ currency,
+ coinMax,
+ coinMin,
+ spec: feespecWithdraw,
+ });
+ const feeDeposit = computeFee({
+ currency,
+ coinMax,
+ coinMin,
+ spec: feespecDeposit,
+ });
+ const feeRefresh = computeFee({
+ currency,
+ coinMax,
+ coinMin,
+ spec: feespecRefresh,
+ });
+ const feeRefund = computeFee({
+ currency,
+ coinMax,
+ coinMin,
+ spec: feespecRefund,
+ });
+
out += `[COIN-${currency}-n${n}-t${stamp}]\n`;
out += `VALUE = ${Amounts.stringify(x)}\n`;
out += `DURATION_WITHDRAW = 7 days\n`;
out += `DURATION_SPEND = 2 years\n`;
out += `DURATION_LEGAL = 6 years\n`;
- out += `FEE_WITHDRAW = ${currency}:0\n`;
- if (args.coincfg.noFees) {
- out += `FEE_DEPOSIT = ${currency}:0\n`;
- } else {
- out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`;
- }
- out += `FEE_REFRESH = ${currency}:0\n`;
- out += `FEE_REFUND = ${currency}:0\n`;
+ out += `FEE_WITHDRAW = ${feeWithdraw}\n`;
+ out += `FEE_DEPOSIT = ${feeDeposit}\n`;
+ out += `FEE_REFRESH = ${feeRefresh}\n`;
+ out += `FEE_REFUND = ${feeRefund}\n`;
out += `RSA_KEYSIZE = 2048\n`;
out += `CIPHER = RSA\n`;
out += "\n";
diff --git a/packages/taler-harness/src/integrationtests/test-account-restrictions.ts b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts
new file mode 100644
index 000000000..c0c85642c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts
@@ -0,0 +1,179 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ j2s,
+ Logger,
+ NotificationType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ ExchangeServiceInterface,
+ GlobalTestState,
+ WalletClient,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ WithdrawViaBankResult,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-account-restrictions.ts");
+
+/**
+ * Test for credit/debit account restrictions.
+ */
+export async function runAccountRestrictionsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ accountRestrictions: [
+ [
+ "debit-restriction",
+ "regex",
+ "payto://x-taler-bank/.*/foo-.*",
+ "bla",
+ "{}",
+ ],
+ [
+ "credit-restriction",
+ "regex",
+ "payto://x-taler-bank/.*/foo-.*",
+ "bla",
+ "{}",
+ ],
+ ],
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawalResult = await myWithdrawViaBank(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:20",
+ acctname: "foo-123",
+ });
+
+ await withdrawalResult.withdrawalFinishedCond;
+
+ // When withdrawing from an account that doesn't begin with "foo-",
+ // it fails.
+ await t.assertThrowsAsync(async () => {
+ await myWithdrawViaBank(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:20",
+ acctname: "bar-123",
+ });
+ });
+
+ // Invalid account, does not start with "foo-"
+ const err = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.CheckDeposit, {
+ amount: "TESTKUDOS:5",
+ depositPaytoUri: "payto://x-taler-bank/localhost/bar-42",
+ });
+ });
+
+ logger.info(`checkResp ${j2s(err)}`);
+
+ // Valid account
+ await walletClient.call(WalletApiOperation.CheckDeposit, {
+ amount: "TESTKUDOS:5",
+ depositPaytoUri: "payto://x-taler-bank/localhost/foo-42",
+ });
+}
+
+export async function myWithdrawViaBank(
+ t: GlobalTestState,
+ p: {
+ walletClient: WalletClient;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeServiceInterface;
+ amount: AmountString | string;
+ restrictAge?: number;
+ acctname: string;
+ },
+): Promise<WithdrawViaBankResult> {
+ const { walletClient: wallet, bankClient, exchange, amount } = p;
+ await bankClient.registerAccountExtended({
+ name: p.acctname,
+ password: "test",
+ username: p.acctname,
+ });
+ const user = {
+ password: "test",
+ username: p.acctname,
+ };
+ const accountPaytoUri = `payto://x-taler-bank/localhost/${p.acctname}?receiver-name=${p.acctname}`;
+ const bankClient2 = new TalerCorebankApiClient(bankClient.baseUrl);
+ bankClient2.setAuth({
+ username: user.username,
+ password: user.password,
+ });
+
+ const wop = await bankClient2.createWithdrawalOperation(
+ user.username,
+ amount,
+ );
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ });
+
+ // Withdraw (AKA select)
+
+ const acceptRes = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ },
+ );
+
+ const withdrawalFinishedCond = wallet.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId,
+ );
+
+ // Confirm it
+
+ await bankClient2.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ return {
+ accountPaytoUri,
+ withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
+ };
+}
+
+runAccountRestrictionsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts
index a0e97c218..aa107696c 100644
--- a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts
@@ -24,7 +24,7 @@ import {
TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV3,
withdrawViaBankV3,
@@ -83,7 +83,7 @@ export async function runAgeRestrictionsDepositTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10" as AmountString,
- depositPaytoUri: generateRandomPayto("foo"),
+ depositPaytoUri: getTestHarnessPaytoForLabel("foo"),
transactionId: depositTxId,
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts
index 58f8bb106..544957185 100644
--- a/packages/taler-harness/src/integrationtests/test-bank-api.ts
+++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts
@@ -30,7 +30,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
@@ -66,7 +66,7 @@ export async function runBankApiTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
let wireGatewayApiBaseUrl = new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href;
await exchange.addBankAccount("1", {
@@ -95,13 +95,13 @@ export async function runBankApiTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
index 34d18d87d..48502f6b7 100644
--- a/packages/taler-harness/src/integrationtests/test-currency-scope.ts
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -26,7 +26,7 @@ import {
GlobalTestState,
HarnessExchangeBankAccount,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -81,7 +81,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
).href,
accountName: "myexchange",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"),
};
let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
@@ -91,7 +91,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
).href,
accountName: "myexchange2",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange2"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange2"),
};
bank.setSuggestedExchange(
@@ -151,7 +151,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -160,7 +160,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts
index 0879c9e9f..654829c91 100644
--- a/packages/taler-harness/src/integrationtests/test-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-deposit.ts
@@ -25,7 +25,7 @@ import {
j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV3,
withdrawViaBankV3,
@@ -51,6 +51,23 @@ export async function runDepositTest(t: GlobalTestState) {
await withdrawalResult.withdrawalFinishedCond;
+ const depositPaytoUri = getTestHarnessPaytoForLabel("foo");
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:19.53");
+
+ const maxDepositResp = await walletClient.call(
+ WalletApiOperation.GetMaxDepositAmount,
+ {
+ currency: "TESTKUDOS",
+ depositPaytoUri,
+ },
+ );
+
+ t.assertAmountEquals(maxDepositResp.rawAmount, "TESTKUDOS:19.09");
+ t.assertAmountEquals(maxDepositResp.effectiveAmount, "TESTKUDOS:19.53");
+
const dgIdResp = await walletClient.client.call(
WalletApiOperation.GenerateDepositGroupTxId,
{},
@@ -77,7 +94,7 @@ export async function runDepositTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10" as AmountString,
- depositPaytoUri: generateRandomPayto("foo"),
+ depositPaytoUri,
transactionId: depositTxId,
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts
index 801162ac8..9bdb0d93b 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts
@@ -36,7 +36,7 @@ import {
GlobalTestState,
MerchantService,
WalletCli,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
@@ -74,7 +74,7 @@ export async function runExchangeManagementFaultTest(
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -111,13 +111,13 @@ export async function runExchangeManagementFaultTest(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts b/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts
new file mode 100644
index 000000000..a66d94b57
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts
@@ -0,0 +1,114 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ ExchangeUpdateStatus,
+ TalerErrorCode,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ GlobalTestState,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+/**
+ * Test the wallet's behavior when the exchange switches to a completely
+ * new master public keyy.
+ */
+export async function runExchangeMasterPubChangeTest(
+ t: GlobalTestState,
+): Promise<void> {
+ // Set up test environment
+
+ const { walletClient, exchange, bankClient, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ const wres = await withdrawViaBankV3(t, {
+ walletClient,
+ amount: "TESTKUDOS:10",
+ bankClient,
+ exchange,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ t.logStep("withdrawal-done");
+
+ const exchangesListOld = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ console.log(j2s(exchangesListOld));
+
+ await exchange.stop();
+
+ // Instead of reconfiguring the old exchange, we just create a new exchange here
+ // that runs under the same base URL as the old exchange.
+
+ const db2 = await setupDb(t, {
+ nameSuffix: "e2",
+ });
+ const exchange2 = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db2.connStr,
+ });
+
+ await exchange2.addBankAccount("1", exchangeBankAccount);
+ exchange2.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
+ await exchange2.start();
+
+ t.logStep("exchange-restarted");
+
+ const err = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchange.baseUrl,
+ force: true,
+ });
+ });
+
+ console.log("updateExchangeEntry err:", j2s(err));
+
+ const exchangesList = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ console.log(j2s(exchangesList));
+
+ t.assertDeepEqual(
+ exchangesList.exchanges[0].exchangeUpdateStatus,
+ ExchangeUpdateStatus.UnavailableUpdate,
+ );
+ t.assertDeepEqual(
+ exchangesList.exchanges[0].unavailableReason?.code,
+ TalerErrorCode.WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT,
+ );
+}
+
+runExchangeMasterPubChangeTest.suites = ["wallet", "exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
index 6666e2d0b..68dd58d3e 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
@@ -148,6 +148,7 @@ export async function runExchangePurseTest(t: GlobalTestState) {
contribution: amount,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
+ feeDeposit: d1.fees.feeDeposit,
};
const depositSigsResp = await cryptoApi.signPurseDeposits({
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index 4f2fb1ee4..828289373 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -35,7 +35,7 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
GlobalTestState,
MerchantService,
setupDb,
@@ -128,7 +128,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -171,13 +171,13 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
index 6ae7b5de8..b08fce4cb 100644
--- a/packages/taler-harness/src/integrationtests/test-fee-regression.ts
+++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
@@ -27,7 +27,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -70,7 +70,7 @@ export async function createMyTestkudosEnvironment(
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -170,7 +170,7 @@ export async function createMyTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
@@ -189,6 +189,7 @@ export async function createMyTestkudosEnvironment(
walletClient,
walletService,
bankClient,
+ bank,
exchangeBankAccount: {
accountName: "",
accountPassword: "",
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts
new file mode 100644
index 000000000..5e7a4756e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts
@@ -0,0 +1,266 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ encodeCrock,
+ ExchangeWalletKycStatus,
+ hashPaytoUri,
+ j2s,
+ TalerCorebankApiClient,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "balance");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:10");
+ config.setString("KYC-RULE-R1", "timeframe", "forever");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycBalanceWithdrawalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:20",
+ bankClient,
+ exchange,
+ walletClient,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycRequired,
+ },
+ });
+
+ {
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+ console.log(j2s(exchangeEntry));
+ }
+
+ await walletClient.call(WalletApiOperation.TestingWaitExchangeState, {
+ exchangeBaseUrl: exchange.baseUrl,
+ walletKycStatus: ExchangeWalletKycStatus.Legi,
+ });
+
+ {
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+ console.log(j2s(exchangeEntry));
+ t.assertDeepEqual(
+ exchangeEntry.walletKycStatus,
+ ExchangeWalletKycStatus.Legi,
+ );
+
+ const kycReservePub = exchangeEntry.walletKycReservePub;
+ t.assertTrue(!!kycReservePub);
+
+ // FIXME: Create/user helper function for this!
+ const hPayto = hashPaytoUri(
+ `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`,
+ );
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: encodeCrock(hPayto),
+ });
+ }
+
+ // Now after KYC is done for the balance, the withdrawal should finish
+ await wres.withdrawalFinishedCond;
+}
+
+runKycBalanceWithdrawalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts
new file mode 100644
index 000000000..fb0e7d79e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts
@@ -0,0 +1,291 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "aggregate");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycDepositAggregateTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:50",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const depositResp = await walletClient.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10",
+ depositPaytoUri: wres.accountPaytoUri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Track,
+ },
+ });
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ console.log("waiting for kyc-required");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: depositResp.transactionId,
+ },
+ );
+
+ const kycPaytoHash = txDetails.kycPaytoHash;
+
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ await exchange.runTransferOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runKycDepositAggregateTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts
new file mode 100644
index 000000000..b91c32a71
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts
@@ -0,0 +1,367 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Logger,
+ parsePaytoUri,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WireGatewayApiClient,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-kyc-deposit-deposit.ts");
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ const wireGatewayApiBaseUrl = new URL(
+ `accounts/${exchangeBankUsername}/taler-wire-gateway/`,
+ bank.corebankApiBaseUrl,
+ ).href;
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "deposit");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: exchangePaytoUri,
+ wireGatewayApiBaseUrl,
+ },
+ };
+}
+
+export async function runKycDepositDepositKyctransferTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ walletClient,
+ bankClient,
+ exchange,
+ amlKeypair,
+ exchangeBankAccount,
+ } = await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:50",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const depositPaytoUri = getTestHarnessPaytoForLabel("deposit-test");
+
+ await bankClient.registerAccountExtended({
+ name: "deposit-test",
+ password: "test",
+ username: "deposit-test",
+ is_taler_exchange: false,
+ payto_uri: depositPaytoUri,
+ });
+
+ const depositResp = await walletClient.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10",
+ depositPaytoUri,
+ },
+ );
+
+ console.log("waiting for kyc-required");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycAuthRequired,
+ },
+ });
+
+ t.logStep("kyc-auth-requested");
+
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ },
+ );
+
+ {
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: depositResp.transactionId,
+ },
+ );
+
+ t.assertDeepEqual(txDetails.type, TransactionType.Deposit);
+ const kycTx = txDetails.kycAuthTransferInfo;
+ t.assertTrue(!!kycTx);
+
+ logger.info(`account pub: ${kycTx.accountPub}`);
+
+ await wireGatewayApiClient.adminAddKycauth({
+ amount: "TESTKUDOS:0.1",
+ debitAccountPayto: depositPaytoUri,
+ accountPub: kycTx.accountPub,
+ });
+ }
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: depositResp.transactionId,
+ },
+ );
+
+ {
+ const kycAuthCreditPayto =
+ txDetails.kycAuthTransferInfo?.creditPaytoUris[0];
+ t.assertTrue(!!kycAuthCreditPayto);
+ const p = parsePaytoUri(kycAuthCreditPayto);
+ t.assertTrue(!!p);
+ t.assertAmountEquals(p.params["amount"], "TESTKUDOS:0.01");
+ }
+
+ const kycPaytoHash = txDetails.kycPaytoHash;
+
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ logger.info(`made decision to have no rules on ${kycPaytoHash}`);
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: [
+ {
+ major: TransactionMajorState.Done,
+ },
+ {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Track,
+ },
+ ],
+ });
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ await exchange.runTransferOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runKycDepositDepositKyctransferTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts
new file mode 100644
index 000000000..10f34a56a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts
@@ -0,0 +1,297 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Logger,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-kyc-deposit-deposit.ts");
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "deposit");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycDepositDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:50",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const depositResp = await walletClient.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10",
+ depositPaytoUri: wres.accountPaytoUri,
+ },
+ );
+
+ console.log("waiting for kyc-required");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: depositResp.transactionId,
+ },
+ );
+
+ const kycPaytoHash = txDetails.kycPaytoHash;
+
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ logger.info(`made decision to have no rules on ${kycPaytoHash}`);
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: [
+ {
+ major: TransactionMajorState.Done,
+ },
+ {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Track,
+ },
+ ],
+ });
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ await exchange.runTransferOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runKycDepositDepositTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts b/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts
new file mode 100644
index 000000000..fbbaf382d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts
@@ -0,0 +1,248 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ encodeCrock,
+ ExchangeWalletKycStatus,
+ hashPaytoUri,
+ j2s,
+ TalerCorebankApiClient,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import { EnvOptions, postAmlDecisionNoRules } from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "balance");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "forever");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ amlKeypair,
+ };
+}
+
+export async function runKycExchangeWalletTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ await walletClient.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await walletClient.call(WalletApiOperation.StartExchangeWalletKyc, {
+ amount: "TESTKUDOS:20",
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitExchangeWalletKyc, {
+ amount: "TESTKUDOS:20",
+ exchangeBaseUrl: exchange.baseUrl,
+ passed: false,
+ });
+
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ console.log(j2s(exchangeEntry));
+
+ t.assertDeepEqual(
+ exchangeEntry.walletKycStatus,
+ ExchangeWalletKycStatus.Legi,
+ );
+
+ const kycReservePub = exchangeEntry.walletKycReservePub;
+
+ t.assertTrue(!!kycReservePub);
+
+ // FIXME: Create/user helper function for this!
+ const hPayto = hashPaytoUri(
+ `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`,
+ );
+
+ console.log(`hPayto: ${hPayto}`);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: encodeCrock(hPayto),
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitExchangeWalletKyc, {
+ amount: "TESTKUDOS:20",
+ exchangeBaseUrl: exchange.baseUrl,
+ passed: true,
+ });
+}
+
+runKycExchangeWalletTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts
new file mode 100644
index 000000000..6a8a13ab9
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts
@@ -0,0 +1,311 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForAny,
+ codecForKycProcessClientInformation,
+ decodeCrock,
+ encodeCrock,
+ j2s,
+ signAmlQuery,
+ TalerCorebankApiClient,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ harnessHttpLib,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import { EnvOptions, withdrawViaBankV3 } from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1 M2");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("KYC-MEASURE-M2", "check_name", "C2");
+ config.setString("KYC-MEASURE-M2", "context", "{}");
+ config.setString("KYC-MEASURE-M2", "program", "P2");
+
+ config.setString(
+ "AML-PROGRAM-P1",
+ "command",
+ "taler-exchange-helper-measure-test-form",
+ );
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString(
+ "AML-PROGRAM-P1",
+ "description",
+ "test for full_name and birthdate",
+ );
+ config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("AML-PROGRAM-P2", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P2", "enabled", "true");
+ config.setString("AML-PROGRAM-P2", "description", "does nothing");
+ config.setString("AML-PROGRAM-P2", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P2", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "FORM");
+ config.setString("KYC-CHECK-C1", "form_name", "myform");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C2", "type", "INFO");
+ config.setString("KYC-CHECK-C2", "description", "my check info!");
+ config.setString("KYC-CHECK-C2", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C2", "fallback", "M2");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycFormWithdrawalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:20",
+ bankClient,
+ exchange,
+ walletClient,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: wres.transactionId,
+ },
+ );
+
+ console.log(j2s(txDetails));
+ const accessToken = txDetails.kycAccessToken;
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ console.log(j2s(clientInfo));
+
+ const kycId = clientInfo.requirements.find((x) => x.id != null)?.id;
+ t.assertTrue(!!kycId);
+
+ const uploadResp = await harnessHttpLib.fetch(
+ new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: "full_name=Alice+Abc&birthdate=2000-01-01",
+ },
+ );
+
+ console.log("resp status", uploadResp.status);
+
+ t.assertDeepEqual(uploadResp.status, 204);
+
+ const sig = signAmlQuery(decodeCrock(amlKeypair.priv));
+
+ const decisionsResp = await harnessHttpLib.fetch(
+ new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
+ {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(sig),
+ },
+ },
+ );
+
+ const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny());
+ console.log(j2s(decisions));
+
+ t.assertDeepEqual(decisionsResp.status, 200);
+
+ // KYC should pass now
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runKycFormWithdrawalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts
new file mode 100644
index 000000000..3b32656ae
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts
@@ -0,0 +1,292 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration, j2s, TalerCorebankApiClient } from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ harnessHttpLib,
+ MerchantService,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ makeTestPaymentV2,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+ merchant: MerchantService;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "aggregate");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ merchant.addExchange(exchange);
+
+ if (opts.additionalMerchantConfig) {
+ opts.additionalMerchantConfig(merchant);
+ }
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ merchant,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycMerchantAggregateTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { merchant, walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:50",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient,
+ order: {
+ amount: "TESTKUDOS:20",
+ summary: "hello",
+ },
+ });
+
+ await exchange.runAggregatorOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ await exchange.runTransferOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ t.logStep("start-run-kyccheck");
+
+ await merchant.runReconciliationOnceWithTimetravel({
+ timetravelMicroseconds: 1000 * 1000 * 60 * 60,
+ });
+
+ t.logStep("start-request-kyc");
+ const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl());
+ const resp = await harnessHttpLib.fetch(kycStatusUrl.href);
+
+ console.log(`mechant kyc status: ${resp.status}`);
+
+ t.assertDeepEqual(resp.status, 200);
+
+ console.log(j2s(await resp.json()));
+}
+
+runKycMerchantAggregateTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts
new file mode 100644
index 000000000..73449ecb7
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts
@@ -0,0 +1,413 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForAccountKycRedirects,
+ codecForKycProcessClientInformation,
+ codecForQueryInstancesResponse,
+ Duration,
+ encodeCrock,
+ hashPaytoUri,
+ j2s,
+ Logger,
+ MerchantAccountKycRedirectsResponse,
+ TalerCorebankApiClient,
+ WireGatewayApiClient,
+} from "@gnu-taler/taler-util";
+import {
+ readResponseJsonOrThrow,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ harnessHttpLib,
+ MerchantService,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+const logger = new Logger(`test-kyc-merchant-deposit.ts`);
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+ merchant: MerchantService;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ const wireGatewayApiBaseUrl = new URL(
+ `accounts/${exchangeBankUsername}/taler-wire-gateway/`,
+ bank.corebankApiBaseUrl,
+ ).href;
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "deposit");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ merchant.addExchange(exchange);
+
+ if (opts.additionalMerchantConfig) {
+ opts.additionalMerchantConfig(merchant);
+ }
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ merchant,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl,
+ },
+ };
+}
+
+export async function runKycMerchantDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ merchant,
+ walletClient,
+ bankClient,
+ exchange,
+ exchangeBankAccount,
+ amlKeypair,
+ } = await createKycTestkudosEnvironment(t);
+
+ let accountPub: string;
+
+ {
+ const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl());
+ const resp = await harnessHttpLib.fetch(instanceUrl.href);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForQueryInstancesResponse(),
+ );
+ accountPub = parsedResp.merchant_pub;
+ }
+
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ },
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:50",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ let kycRespOne: MerchantAccountKycRedirectsResponse | undefined = undefined;
+
+ while (1) {
+ const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl())
+ .href;
+ logger.info(`requesting GET ${kycStatusUrl}`);
+ const resp = await harnessHttpLib.fetch(kycStatusUrl);
+ if (resp.status === 200) {
+ kycRespOne = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycRedirects(),
+ );
+ break;
+ }
+ // Wait 500ms
+ await new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ }
+
+ t.assertTrue(!!kycRespOne);
+
+ logger.info(`mechant kyc status: ${j2s(kycRespOne)}`);
+
+ await wireGatewayApiClient.adminAddKycauth({
+ amount: "TESTKUDOS:0.1",
+ debitAccountPayto: kycRespOne.kyc_data[0].payto_uri,
+ accountPub,
+ });
+
+ let kycRespTwo: MerchantAccountKycRedirectsResponse | undefined = undefined;
+
+ // We do this in a loop as a work-around.
+ // Not exactly the correct behavior from the merchant right now.
+ while (true) {
+ const kycStatusLongpollUrl = new URL(
+ "private/kyc",
+ merchant.makeInstanceBaseUrl(),
+ );
+ kycStatusLongpollUrl.searchParams.set("lpt", "1");
+ const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href);
+ t.assertDeepEqual(resp.status, 200);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycRedirects(),
+ );
+ logger.info(`kyc resp 2: ${j2s(parsedResp)}`);
+ if (parsedResp.kyc_data[0].payto_kycauths == null) {
+ kycRespTwo = parsedResp;
+ break;
+ }
+ // Wait 500ms
+ await new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ }
+
+ t.assertTrue(!!kycRespTwo);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: encodeCrock(hashPaytoUri(kycRespTwo.kyc_data[0].payto_uri)),
+ });
+
+ // We do this in a loop as a work-around.
+ // Not exactly the correct behavior from the merchant right now.
+ while (true) {
+ const kycStatusLongpollUrl = new URL(
+ "private/kyc",
+ merchant.makeInstanceBaseUrl(),
+ );
+ kycStatusLongpollUrl.searchParams.set("lpt", "3");
+ const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href);
+ t.assertDeepEqual(resp.status, 200);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycRedirects(),
+ );
+ logger.info(`kyc resp 3: ${j2s(parsedResp)}`);
+ if ((parsedResp.kyc_data[0].limits?.length ?? 0) == 0) {
+ break;
+ }
+
+ const accessToken = parsedResp.kyc_data[0].access_token;
+
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ logger.info(`kyc-info: ${j2s(clientInfo)}`);
+
+ // Wait 500ms
+ await new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ }
+}
+
+runKycMerchantDepositTest.suites = ["wallet", "merchant", "kyc"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts b/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts
new file mode 100644
index 000000000..c0d3881a0
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts
@@ -0,0 +1,410 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForAny,
+ codecForKycProcessClientInformation,
+ decodeCrock,
+ encodeCrock,
+ j2s,
+ signAmlQuery,
+ TalerCorebankApiClient,
+ TalerProtocolTimestamp,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ harnessHttpLib,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ EnvOptions,
+ postAmlDecision,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1 M2");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("KYC-MEASURE-M2", "check_name", "C2");
+ config.setString("KYC-MEASURE-M2", "context", "{}");
+ config.setString("KYC-MEASURE-M2", "program", "P2");
+
+ config.setString("KYC-MEASURE-M3", "check_name", "C3");
+ config.setString("KYC-MEASURE-M3", "context", "{}");
+ config.setString("KYC-MEASURE-M3", "program", "P2");
+
+ config.setString(
+ "AML-PROGRAM-P1",
+ "command",
+ "taler-exchange-helper-measure-test-form",
+ );
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString(
+ "AML-PROGRAM-P1",
+ "description",
+ "test for full_name and birthdate",
+ );
+ config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("AML-PROGRAM-P2", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P2", "enabled", "true");
+ config.setString("AML-PROGRAM-P2", "description", "does nothing");
+ config.setString("AML-PROGRAM-P2", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P2", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "FORM");
+ config.setString("KYC-CHECK-C1", "form_name", "myform");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C2", "type", "INFO");
+ config.setString("KYC-CHECK-C2", "description", "my check info!");
+ config.setString("KYC-CHECK-C2", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C2", "fallback", "M2");
+
+ config.setString("KYC-CHECK-C3", "type", "INFO");
+ config.setString("KYC-CHECK-C3", "description", "this is info c3");
+ config.setString("KYC-CHECK-C3", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C3", "fallback", "M2");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+/**
+ * Test setting a `new_measure` as the AML officer.
+ */
+export async function runKycNewMeasureTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+ let kycPaytoHash: string | undefined;
+ let accessToken: string | undefined;
+ let firstTransaction: string | undefined;
+
+ {
+ const wres = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:20",
+ bankClient,
+ exchange,
+ walletClient,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: wres.transactionId,
+ },
+ );
+
+ console.log(j2s(txDetails));
+
+ accessToken = txDetails.kycAccessToken;
+ kycPaytoHash = txDetails.kycPaytoHash;
+ firstTransaction = wres.transactionId;
+ }
+
+ t.assertTrue(!!accessToken);
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ console.log(j2s(clientInfo));
+
+ const kycId = clientInfo.requirements.find((x) => x.id != null)?.id;
+ t.assertTrue(!!kycId);
+
+ const uploadResp = await harnessHttpLib.fetch(
+ new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: "full_name=Alice+Abc&birthdate=2000-01-01",
+ },
+ );
+
+ console.log("resp status", uploadResp.status);
+
+ t.assertDeepEqual(uploadResp.status, 204);
+
+ const sig = signAmlQuery(decodeCrock(amlKeypair.priv));
+ {
+ const decisionsResp = await harnessHttpLib.fetch(
+ new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
+ {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(sig),
+ },
+ },
+ );
+
+ const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny());
+ console.log(j2s(decisions));
+
+ t.assertDeepEqual(decisionsResp.status, 200);
+ }
+ // KYC should pass now
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: firstTransaction as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+
+ // Now, AML officer takes action and freezes the account, requiring a new_measure
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecision(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ newMeasure: "m3",
+ newRules: {
+ expiration_time: TalerProtocolTimestamp.never(),
+ custom_measures: {},
+ rules: [
+ // No rules!
+ ],
+ },
+ });
+
+
+ {
+ const decisionsResp = await harnessHttpLib.fetch(
+ new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
+ {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(sig),
+ },
+ },
+ );
+
+ const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny());
+ console.log(j2s(decisions));
+
+ t.assertDeepEqual(decisionsResp.status, 200);
+ }
+
+ {
+ const wres = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:21",
+ bankClient,
+ exchange,
+ walletClient,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: wres.transactionId,
+ },
+ );
+ console.log(j2s(txDetails));
+
+ const accessToken = txDetails.kycAccessToken;
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ console.log("second withdrawal, clientInfo:");
+ console.log(j2s(clientInfo));
+ }
+}
+
+runKycNewMeasureTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts
new file mode 100644
index 000000000..0919f6550
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts
@@ -0,0 +1,356 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ j2s,
+ TalerCorebankApiClient,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "merge");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycPeerPullTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Origin wallet for the p2p transaction,
+ // will pay for the invoice.
+ const w0 = await createWalletDaemonWithClient(t, {
+ name: "w0",
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const wres1 = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:20",
+ exchange: exchange,
+ walletClient: w0.walletClient,
+ });
+
+ await wres1.withdrawalFinishedCond;
+
+ const wres2 = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:1",
+ exchange: exchange,
+ walletClient: walletClient,
+ });
+
+ await wres2.withdrawalFinishedCond;
+
+ const pullRes = await doPeerPullCredit(t, {
+ walletClient,
+ amount: "TESTKUDOS:10",
+ summary: "test123",
+ });
+
+ const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: pullRes.transactionId,
+ });
+
+ console.log("tx details", j2s(txDet));
+
+ const kycPaytoHash = txDet.kycPaytoHash;
+
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: pullRes.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ },
+ });
+
+ const prepRes = await w0.walletClient.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: pullRes.talerUri,
+ },
+ );
+
+ await w0.walletClient.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: prepRes.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: pullRes.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+/**
+ * Initiate a pull credit transaction, wait until the transaction
+ * is ready.
+ */
+async function doPeerPullCredit(
+ t: GlobalTestState,
+ args: {
+ walletClient: WalletClient;
+ amount: AmountString;
+ summary?: string;
+ },
+): Promise<{
+ transactionId: string;
+ talerUri: string;
+}> {
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+ const initRet = await args.walletClient.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ partialContractTerms: {
+ amount: args.amount,
+ summary: args.summary ?? "Test P2P Payment",
+ purse_expiration,
+ },
+ },
+ );
+
+ await args.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: initRet.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.MergeKycRequired,
+ },
+ });
+
+ const txDet = await args.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: initRet.transactionId,
+ },
+ );
+
+ t.assertTrue(txDet.type === TransactionType.PeerPullCredit);
+ const talerUri = txDet.talerUri;
+ t.assertTrue(!!talerUri);
+
+ return {
+ transactionId: initRet.transactionId,
+ talerUri,
+ };
+}
+
+runKycPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts
new file mode 100644
index 000000000..6e1a78a26
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts
@@ -0,0 +1,347 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ j2s,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "merge");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycPeerPushTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Origin wallet for the p2p transaction.
+ const w0 = await createWalletDaemonWithClient(t, {
+ name: "w0",
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ bankClient,
+ amount: "TESTKUDOS:20",
+ exchange: exchange,
+ walletClient: w0.walletClient,
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ const pushDebitRes = await doPeerPushDebit(t, {
+ walletClient: w0.walletClient,
+ amount: "TESTKUDOS:10",
+ summary: "Test1",
+ });
+
+ const prepRes = await walletClient.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: pushDebitRes.talerUri,
+ },
+ );
+
+ console.log("prepRes", j2s(prepRes));
+
+ await walletClient.call(WalletApiOperation.ConfirmPeerPushCredit, {
+ transactionId: prepRes.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepRes.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.MergeKycRequired,
+ },
+ });
+
+ const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: prepRes.transactionId,
+ });
+
+ console.log("tx details", j2s(txDet));
+
+ const kycPaytoHash = txDet.kycPaytoHash;
+
+ t.assertTrue(!!kycPaytoHash);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepRes.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+/**
+ * Initiate a push debit transaction, wait until the transaction
+ * is ready.
+ */
+async function doPeerPushDebit(
+ t: GlobalTestState,
+ args: {
+ walletClient: WalletClient;
+ amount: AmountString;
+ summary?: string;
+ },
+): Promise<{
+ transactionId: string;
+ talerUri: string;
+}> {
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+ const initRet = await args.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ amount: args.amount,
+ summary: args.summary ?? "Test P2P Payment",
+ purse_expiration,
+ },
+ },
+ );
+
+ await args.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: initRet.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
+ },
+ });
+
+ const txDet = await args.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: initRet.transactionId,
+ },
+ );
+
+ t.assertTrue(txDet.type === TransactionType.PeerPushDebit);
+ const talerUri = txDet.talerUri;
+ t.assertTrue(!!talerUri);
+
+ return {
+ transactionId: initRet.transactionId,
+ talerUri,
+ };
+}
+
+runKycPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
new file mode 100644
index 000000000..6aa65992a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts
@@ -0,0 +1,322 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ NotificationType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import {
+ createSyncCryptoApi,
+ EddsaKeyPairStrings,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import { EnvOptions, postAmlDecisionNoRules } from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeyPairStrings;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:300");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "verboten");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("AML-PROGRAM-P1", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString("AML-PROGRAM-P1", "description", "this does nothing");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "INFO");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycThresholdWithdrawalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const amount = "TESTKUDOS:20";
+ const user = await bankClient.createRandomBankUser();
+ bankClient.setAuth({
+ username: user.username,
+ password: user.password,
+ });
+
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
+
+ // Hand it to the wallet
+
+ const withdrawalUrlInfo = await walletClient.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const withdrawalAmountInfo = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ amount: withdrawalUrlInfo.amount!,
+ exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl,
+ },
+ );
+
+ // t.assertTrue(!!withdrawalAmountInfo.kycHardLimit);
+ // t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300");
+
+ // Withdraw
+
+ const acceptResp = await walletClient.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const withdrawalTxId = acceptResp.transactionId;
+
+ // Confirm it
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+
+ t.logStep("waiting for pending(kyc-required)");
+
+ const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === withdrawalTxId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.KycRequired
+ ) {
+ return x;
+ }
+ return false;
+ });
+
+ await kycNotificationCond;
+
+ const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: withdrawalTxId,
+ });
+
+ t.assertDeepEqual(txDet.type, TransactionType.Withdrawal);
+
+ const kycPaytoHash = txDet.kycPaytoHash;
+ t.assertTrue(!!kycPaytoHash);
+
+ t.logStep("posting aml decision");
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: kycPaytoHash,
+ });
+
+ t.logStep("waiting for withdrawal to be done");
+
+ const doneNotificationCond = walletClient.waitForNotificationCond((x) => {
+ if (
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === withdrawalTxId &&
+ x.newTxState.major === TransactionMajorState.Done
+ ) {
+ return x;
+ }
+ return false;
+ });
+
+ await doneNotificationCond;
+}
+
+runKycThresholdWithdrawalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts
index 213dd9df4..3a68fcbce 100644
--- a/packages/taler-harness/src/integrationtests/test-kyc.ts
+++ b/packages/taler-harness/src/integrationtests/test-kyc.ts
@@ -25,27 +25,29 @@ import {
TransactionMajorState,
TransactionMinorState,
TransactionType,
+ codecForKycProcessClientInformation,
j2s,
} from "@gnu-taler/taler-util";
-import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
+ BankService,
ExchangeService,
GlobalTestState,
MerchantService,
WalletClient,
WalletService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
+ harnessHttpLib,
setupDb,
} from "../harness/harness.js";
import { EnvOptions, SimpleTestEnvironmentNg3 } from "../harness/helpers.js";
const logger = new Logger("test-kyc.ts");
-export async function createKycTestkudosEnvironment(
+async function createKycTestkudosEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
@@ -76,12 +78,15 @@ export async function createKycTestkudosEnvironment(
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
accountPaytoUri: exchangePaytoUri,
});
@@ -129,11 +134,48 @@ export async function createKycTestkudosEnvironment(
}
await exchange.modifyConfig(async (config) => {
- const myprov = "kyc-provider-myprov";
- config.setString(myprov, "cost", "0");
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("KYC-CHECK-C1", "type", "LINK");
+ config.setString("KYC-CHECK-C1", "provider_id", "MYPROV");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+
+ config.setString(
+ "AML-PROGRAM-P1",
+ "command",
+ "taler-exchange-helper-measure-test-form",
+ );
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString(
+ "AML-PROGRAM-P1",
+ "description",
+ "test for full_name and birthdate",
+ );
+ config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ const myprov = "KYC-PROVIDER-MYPROV";
config.setString(myprov, "logic", "oauth2");
- config.setString(myprov, "provided_checks", "dummy1");
- config.setString(myprov, "user_type", "individual");
+ config.setString(
+ myprov,
+ "converter",
+ "taler-exchange-kyc-oauth2-test-converter.sh",
+ );
config.setString(myprov, "kyc_oauth2_validity", "forever");
config.setString(
myprov,
@@ -164,17 +206,6 @@ export async function createKycTestkudosEnvironment(
"operation_type",
"withdraw",
);
- config.setString(
- "kyc-legitimization-withdraw1",
- "required_checks",
- "dummy1",
- );
- config.setString("kyc-legitimization-withdraw1", "timeframe", "1d");
- config.setString(
- "kyc-legitimization-withdraw1",
- "threshold",
- "TESTKUDOS:5",
- );
});
await exchange.start();
@@ -188,7 +219,7 @@ export async function createKycTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -197,7 +228,7 @@ export async function createKycTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -234,12 +265,13 @@ export async function createKycTestkudosEnvironment(
merchant,
walletClient,
walletService,
+ bank,
bankClient,
exchangeBankAccount: {
- accountName: '',
- accountPassword: '',
- accountPaytoUri: '',
- wireGatewayApiBaseUrl: '',
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
},
};
}
@@ -314,7 +346,10 @@ async function runTestfakeKycService(): Promise<TestfakeKycService> {
JSON.stringify({
status: "success",
data: {
- id: "foobar",
+ id: "Foobar",
+ first_name: "Alice",
+ last_name: "Abc",
+ birthdate: "2000-01-01",
},
}),
);
@@ -410,26 +445,48 @@ export async function runKycTest(t: GlobalTestState) {
);
t.assertDeepEqual(txState.type, TransactionType.Withdrawal);
+ const paytoHash = txState.kycPaytoHash;
- const kycUrl = txState.kycUrl;
-
- t.assertTrue(!!kycUrl);
-
- logger.info(`kyc URL is ${kycUrl}`);
+ t.assertTrue(!!txState.kycUrl);
+ t.assertTrue(!!paytoHash);
// We now simulate the user interacting with the KYC service,
// which would usually done in the browser.
- const httpLib = createPlatformHttpLib({
- enableThrottling: false,
- });
- const kycServerResp = await httpLib.fetch(kycUrl);
- const kycLoginResp = await kycServerResp.json();
- logger.info(`kyc server resp: ${j2s(kycLoginResp)}`);
- const kycProofUrl = kycLoginResp.redirect_uri;
+ const accessToken = txState.kycAccessToken;
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${txState.kycAccessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ console.log(j2s(clientInfo));
+
+ const kycId = clientInfo.requirements.find((x) => x.id != null)?.id;
+ t.assertTrue(!!kycId);
+
+ const startResp = await harnessHttpLib.fetch(
+ new URL(`kyc-start/${kycId}`, exchange.baseUrl).href,
+ {
+ method: "POST",
+ body: {},
+ },
+ );
+
+ logger.info(`kyc-start resp status: ${startResp.status}`);
+ logger.info(j2s(startResp.json()));
+
// We need to "visit" the KYC proof URL at least once to trigger the exchange
// asking for the KYC status.
- const proofHttpResp = await httpLib.fetch(kycProofUrl);
+ const proofUrl = new URL(`kyc-proof/MYPROV`, exchange.baseUrl);
+ proofUrl.searchParams.set("state", paytoHash);
+ proofUrl.searchParams.set("code", "code_is_ok");
+ const proofHttpResp = await harnessHttpLib.fetch(proofUrl.href);
logger.info(`proof resp status ${proofHttpResp.status}`);
logger.info(`resp headers ${j2s(proofHttpResp.headers.toJSON())}`);
if (
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
index 01b20ddbf..1b67332d8 100644
--- a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
@@ -35,7 +35,7 @@ import {
GlobalTestState,
LibeufinBankService,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
generateRandomTestIban,
setupDb,
} from "../harness/harness.js";
@@ -74,7 +74,7 @@ export async function runLibeufinBankTest(t: GlobalTestState) {
const exchangeBankUsername = "exchange";
const exchangeBankPw = "mypw";
- const exchangePayto = generateRandomPayto(exchangeBankUsername);
+ const exchangePayto = getTestHarnessPaytoForLabel(exchangeBankUsername);
const wireGatewayApiBaseUrl = new URL(
"accounts/exchange/taler-wire-gateway/",
bank.baseUrl,
@@ -108,13 +108,13 @@ export async function runLibeufinBankTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
const { walletClient } = await createWalletDaemonWithClient(t, {
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-categories.ts b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts
new file mode 100644
index 000000000..21ccfef3a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts
@@ -0,0 +1,178 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { URL, j2s } from "@gnu-taler/taler-util";
+import {
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ getTestHarnessPaytoForLabel,
+ harnessHttpLib,
+ setupDb,
+} from "../harness/harness.js";
+
+/**
+ * Do basic checks on instance management and authentication.
+ */
+export async function runMerchantCategoriesTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ // We add the exchange to the config, but note that the exchange won't be started.
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ // Base URL for the default instance.
+ const baseUrl = merchant.makeInstanceBaseUrl();
+
+ {
+ const r = await harnessHttpLib.fetch(new URL("config", baseUrl).href);
+ const data = await r.json();
+ console.log(data);
+ t.assertDeepEqual(data.currency, "TESTKUDOS");
+ }
+
+ // Instances should initially be empty
+ {
+ const r = await harnessHttpLib.fetch(
+ new URL("management/instances", baseUrl).href,
+ );
+ const data = await r.json();
+ t.assertDeepEqual(data.instances, []);
+ }
+
+ // Add an instance, no auth!
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ let myNewCategoryId: number;
+
+ {
+ const url = new URL("private/categories", merchant.makeInstanceBaseUrl());
+ const res = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ name: "Snacks",
+ name_i18n: {},
+ },
+ });
+
+ console.log(res.requestUrl);
+ console.log("status", res.status);
+ const categoryJson = await res.json();
+ console.log(categoryJson);
+ t.assertTrue(res.status >= 200 && res.status < 300);
+ myNewCategoryId = categoryJson.category_id;
+ }
+
+ {
+ const url = new URL("private/products", merchant.makeInstanceBaseUrl());
+ const res = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ product_id: "foo",
+ description: "Bla Bla",
+ unit: "item",
+ price: "TESTKUDOS:6",
+ total_stock: -1,
+ },
+ });
+ t.assertTrue(res.status >= 200 && res.status < 300);
+ }
+
+ {
+ const url = new URL("private/products", merchant.makeInstanceBaseUrl());
+ const res = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ product_id: "bar",
+ description: "Bla Bla",
+ unit: "item",
+ price: "TESTKUDOS:2",
+ total_stock: -1,
+ categories: [myNewCategoryId],
+ },
+ });
+ t.assertTrue(res.status >= 200 && res.status < 300);
+ }
+
+ {
+ const url = new URL("private/products", merchant.makeInstanceBaseUrl());
+ const res = await harnessHttpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ product_id: "baz",
+ description: "Eggs",
+ unit: "item",
+ price: "TESTKUDOS:42",
+ total_stock: -1,
+ },
+ });
+ t.assertTrue(res.status >= 200 && res.status < 300);
+ }
+
+ {
+ const posUrl = new URL("private/pos", merchant.makeInstanceBaseUrl());
+ const res = await harnessHttpLib.fetch(posUrl.href, {
+ method: "GET",
+ });
+ const posJson = await res.json();
+ console.log(j2s(posJson));
+ t.assertTrue(res.status >= 200 && res.status < 300);
+
+ t.assertDeepEqual(posJson.products.length, 3);
+
+ const prodFoo = posJson.products.find((x: any) => x.product_id == "foo");
+ console.log(`prod foo`, prodFoo);
+ t.assertTrue(!!prodFoo);
+ // Only default category
+ t.assertDeepEqual(prodFoo.categories, [0]);
+
+ const prodBar = posJson.products.find((x: any) => x.product_id == "bar");
+ console.log(`prod bar`, prodBar);
+ t.assertTrue(!!prodBar);
+ // This should have the one we assigned to it.
+ t.assertDeepEqual(prodBar.categories, [myNewCategoryId]);
+ }
+}
+
+runMerchantCategoriesTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
index 19f89ae2c..8f47eda1b 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -20,9 +20,12 @@
import {
codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
+ j2s,
MerchantApiClient,
PreparePayResultType,
TalerCorebankApiClient,
+ TalerErrorCode,
+ TypedTalerErrorDetail,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { URL } from "url";
@@ -32,9 +35,9 @@ import {
FaultInjectedMerchantService,
} from "../harness/faultInjection.js";
import {
- BankService,
+ BankService,
ExchangeService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
GlobalTestState,
harnessHttpLib,
MerchantService,
@@ -87,12 +90,15 @@ export async function createConfusedMerchantTestkudosEnvironment(
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
accountPaytoUri: exchangePaytoUri,
});
@@ -131,13 +137,13 @@ export async function createConfusedMerchantTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
@@ -258,7 +264,25 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
proposalId: proposalId,
});
- t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Pending);
+
+ console.log(j2s(confirmPayRes.lastError));
+
+ // Merchant should not accept the payment!
+ // Something is clearly wrong, as the exchange now announces
+ // its own base URL and something is wrong.
+
+ // FIXME: This error code should probably be refined in the future.
+
+ t.assertDeepEqual(
+ confirmPayRes.lastError?.code,
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ );
+
+ const err =
+ confirmPayRes.lastError as TypedTalerErrorDetail<TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR>;
+
+ t.assertDeepEqual(err.httpStatusCode, 400);
}
runMerchantExchangeConfusionTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
index c0c9353e4..51beded8d 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
@@ -22,7 +22,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
harnessHttpLib,
setupDb,
} from "../harness/harness.js";
@@ -78,7 +78,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
@@ -88,7 +88,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
index 188451e15..f41b028ae 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -22,7 +22,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
harnessHttpLib,
setupDb,
} from "../harness/harness.js";
@@ -78,7 +78,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
@@ -88,7 +88,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
@@ -98,7 +98,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
index 26e843073..8c1424437 100644
--- a/packages/taler-harness/src/integrationtests/test-multiexchange.ts
+++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts
@@ -26,7 +26,7 @@ import {
GlobalTestState,
HarnessExchangeBankAccount,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -82,7 +82,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
).href,
accountName: "myexchange",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"),
};
let exchangeTwoBankAccount: HarnessExchangeBankAccount = {
@@ -92,7 +92,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
).href,
accountName: "myexchange2",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange2"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange2"),
};
bank.setSuggestedExchange(
@@ -152,7 +152,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -161,7 +161,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
index dabe42a6b..a6836953e 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
@@ -38,7 +38,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -71,7 +71,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -140,7 +140,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
index 3c902ee17..e3f3e18e3 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
@@ -25,7 +25,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -59,7 +59,7 @@ async function setupTest(t: GlobalTestState): Promise<{
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -107,13 +107,13 @@ async function setupTest(t: GlobalTestState): Promise<{
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
index 6de3c2e33..53a3c75f4 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
@@ -62,6 +62,12 @@ const coinConfigList: CoinConfig[] = [
},
];
+/**
+ * Test peer pull payments with a large number of coins.
+ *
+ * Since we use an artificallly large number of coins, this
+ * test is a bit slower than other tests.
+ */
export async function runPeerPullLargeTest(t: GlobalTestState) {
// Set up test environment
@@ -102,6 +108,7 @@ async function checkNormalPeerPull(
wallet1: WalletClient,
wallet2: WalletClient,
): Promise<void> {
+ t.logStep("starting withdrawal");
const withdrawRes = await withdrawViaBankV2(t, {
walletClient: wallet2,
bank,
@@ -111,6 +118,8 @@ async function checkNormalPeerPull(
await withdrawRes.withdrawalFinishedCond;
+ t.logStep("finished withdrawal");
+
const purseExpiration = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
@@ -191,4 +200,4 @@ async function checkNormalPeerPull(
console.log(`txn2: ${j2s(txn2)}`);
}
-runPeerPullLargeTest.suites = ["wallet"];
+runPeerPullLargeTest.suites = ["wallet", "slow"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
index e38b690ab..05a0d2790 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
@@ -69,6 +69,19 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
await withdrawRes.withdrawalFinishedCond;
+ {
+ const maxResp1 = await w1.walletClient.call(
+ WalletApiOperation.GetMaxPeerPushDebitAmount,
+ {
+ currency: "TESTKUDOS",
+ },
+ );
+
+ t.assertDeepEqual(maxResp1.exchangeBaseUrl, exchange.baseUrl);
+ t.assertAmountEquals(maxResp1.rawAmount, "TESTKUDOS:19.1");
+ t.assertAmountEquals(maxResp1.effectiveAmount, "TESTKUDOS:19.53");
+ }
+
const purse_expiration = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
@@ -83,6 +96,7 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
},
);
+ t.assertAmountEquals(checkResp0.amountRaw, "TESTKUDOS:5");
t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:5.49");
{
diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
index 6e02071af..822e465e0 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -54,18 +54,20 @@ export async function runRefundAutoTest(t: GlobalTestState) {
// Test case where the auto-refund happens
{
+ t.logStep("start-test-autorefund");
+
// Set up order.
const orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
- auto_refund: {
- d_us: 3000 * 1000,
- },
+ auto_refund: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
},
refund_delay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 10 }),
),
});
@@ -102,6 +104,8 @@ export async function runRefundAutoTest(t: GlobalTestState) {
console.log(ref);
+ t.logStep("gave-refund");
+
// The wallet should now automatically pick up the refund.
await walletClient.call(
WalletApiOperation.TestingWaitTransactionsFinal,
@@ -122,6 +126,8 @@ export async function runRefundAutoTest(t: GlobalTestState) {
// Now test the case where the auto-refund just expires
+ t.logStep("start-test-expiry");
+
{
// Set up order.
const orderResp = await merchantClient.createOrder({
@@ -129,12 +135,12 @@ export async function runRefundAutoTest(t: GlobalTestState) {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
- auto_refund: {
- d_us: 3000 * 1000,
- },
+ auto_refund: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 5 }),
+ ),
},
refund_delay: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 5 }),
+ Duration.fromSpec({ minutes: 10 }),
),
});
@@ -165,13 +171,13 @@ export async function runRefundAutoTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: r1.transactionId,
txState: {
- major: TransactionMajorState.Pending,
+ major: TransactionMajorState.Finalizing,
minor: TransactionMinorState.AutoRefund,
},
});
// Only time-travel the wallet
await walletClient.call(WalletApiOperation.TestingSetTimetravel, {
- offsetMs: 5000,
+ offsetMs: 10 * 60 * 1000,
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: r1.transactionId,
diff --git a/packages/taler-harness/src/integrationtests/test-repurchase.ts b/packages/taler-harness/src/integrationtests/test-repurchase.ts
new file mode 100644
index 000000000..e2dece8b7
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-repurchase.ts
@@ -0,0 +1,164 @@
+/*
+ This file is part of GNU Taler
+ (C) 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ ConfirmPayResultType,
+ MerchantApiClient,
+ PreparePayResultType,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import {
+ useSharedTestkudosEnvironment,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+export async function runRepurchaseTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange, merchant } =
+ await useSharedTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ } satisfies TalerMerchantApi.Order;
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderOneResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/test",
+ },
+ });
+
+ let orderOneStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderOneResp.order_id,
+ sessionId: "session1",
+ });
+
+ t.assertTrue(orderOneStatus.order_status === "unpaid");
+
+ const preparePayOneResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderOneStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayOneResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayOneResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ const orderTwoResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/test",
+ },
+ });
+
+ let orderTwoStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderTwoResp.order_id,
+ sessionId: "session2",
+ });
+
+ t.assertTrue(orderTwoStatus.order_status === "unpaid");
+
+ const orderLongpollUrl = new URL(
+ `orders/${orderTwoResp.order_id}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+ if (orderTwoResp.token) {
+ orderLongpollUrl.searchParams.set("token", orderTwoResp.token);
+ }
+ orderLongpollUrl.searchParams.set("timeout_ms", "60000");
+ orderLongpollUrl.searchParams.set("session_id", "session2");
+
+ const longpollPromise = harnessHttpLib.fetch(orderLongpollUrl.href);
+
+ const preparePayTwoResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderTwoStatus.taler_pay_uri,
+ },
+ );
+
+ // Repurchase should be detected
+ t.assertTrue(
+ preparePayTwoResult.status === PreparePayResultType.AlreadyConfirmed,
+ );
+
+ t.logStep("start-wait-longpoll-promise");
+ await longpollPromise;
+ t.logStep("done-wait-longpoll-promise");
+
+ // Order three
+
+ const orderThreeResp = await merchantClient.createOrder({
+ order: {
+ summary: "Buy me",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/test",
+ },
+ });
+
+ let orderThreeStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderThreeResp.order_id,
+ // Go back to session1
+ sessionId: "session1",
+ });
+
+ t.assertTrue(orderThreeStatus.order_status === "unpaid");
+
+ const preparePayThreeResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderThreeStatus.taler_pay_uri,
+ },
+ );
+
+ // Repurchase should be detected
+ t.assertTrue(
+ preparePayThreeResult.status === PreparePayResultType.AlreadyConfirmed,
+ );
+}
+
+runRepurchaseTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 65aa86f98..d88a74150 100644
--- a/packages/taler-harness/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -31,7 +31,7 @@ import {
WalletCli,
WalletClient,
delayMs,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -51,7 +51,7 @@ async function revokeAllWalletCoins(req: {
console.log(coinDump);
const usedDenomHashes = new Set<string>();
for (const coin of coinDump.coins) {
- usedDenomHashes.add(coin.denom_pub_hash);
+ usedDenomHashes.add(coin.denomPubHash);
}
for (const x of usedDenomHashes.values()) {
await exchange.revokeDenomination(x);
@@ -93,7 +93,7 @@ async function createTestEnvironment(
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -153,13 +153,13 @@ async function createTestEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
@@ -179,6 +179,7 @@ async function createTestEnvironment(
merchant,
walletClient,
walletService,
+ bank,
bankClient,
exchangeBankAccount: {
accountName: "",
@@ -239,7 +240,7 @@ export async function runRevocationTest(t: GlobalTestState) {
const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
console.log(coinDump);
- const coinPubList = coinDump.coins.map((x) => x.coin_pub);
+ const coinPubList = coinDump.coins.map((x) => x.coinPub);
await walletClient.call(WalletApiOperation.ForceRefresh, {
refreshCoinSpecs: coinPubList.map((x) => ({ coinPub: x })),
});
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
index 046bd5aed..27bf7bdd6 100644
--- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -33,7 +33,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -74,7 +74,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -120,13 +120,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
index 4ee3a86e9..1ba7dc2ad 100644
--- a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
@@ -97,7 +97,7 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) {
// Now we also let the wallet time travel
- walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ await walletClient.call(WalletApiOperation.TestingSetTimetravel, {
offsetMs: Duration.toMilliseconds(timetravelDuration),
});
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
index c37a6e482..a8e62aee8 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
@@ -21,7 +21,6 @@ import {
Amounts,
Duration,
MerchantApiClient,
- MerchantContractTerms,
PreparePayResultType,
TalerMerchantApi,
} from "@gnu-taler/taler-util";
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
index 66f985114..70f8989c8 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
@@ -26,7 +26,7 @@ import {
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig } from "../harness/denomStructures.js";
-import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV3,
createWalletDaemonWithClient,
@@ -104,7 +104,7 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) {
},
});
- const userPayto = generateRandomPayto("foo");
+ const userPayto = getTestHarnessPaytoForLabel("foo");
const bal = await w1.call(WalletApiOperation.GetBalances, {});
console.log(`balance: ${j2s(bal)}`);
@@ -114,7 +114,7 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) {
});
console.log(`balance details: ${j2s(balDet)}`);
- const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, {
+ const depositCheckResp = await w1.call(WalletApiOperation.CheckDeposit, {
amount: "TESTKUDOS:18" as AmountString,
depositPaytoUri: userPayto,
});
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts b/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts
index bcd7de74b..ef4166760 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts
@@ -21,14 +21,10 @@ import { AmountString } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
- ExchangeService,
- FakebankService,
GlobalTestState,
- MerchantService,
- WalletCli,
- generateRandomPayto,
setupDb,
} from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "harness/helpers.js";
/**
* Test that run-until-done of taler-wallet-cli terminates.
@@ -38,64 +34,17 @@ export async function runWalletCliTerminationTest(t: GlobalTestState) {
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
- const bank = await FakebankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
+ const {
+ exchange,
+ bankClient,
+ walletClient,
+ } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {});
- exchange.addCoinConfigList(coinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- await merchant.addInstanceWithWireAccount({
- id: "default",
- name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
- });
-
- const wallet = new WalletCli(t, "wallet");
-
- await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
- corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
+ corebankApiBaseUrl: bankClient.baseUrl,
exchangeBaseUrl: exchange.baseUrl,
amount: "TESTKUDOS:20" as AmountString,
});
-
- await wallet.runUntilDone();
}
runWalletCliTerminationTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
index a089d99b5..1d57e3458 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -25,7 +25,6 @@ import {
TalerError,
} from "@gnu-taler/taler-util";
import {
- applyRunConfigDefaults,
CryptoDispatcher,
SynchronousCryptoWorkerFactoryPlain,
} from "@gnu-taler/taler-wallet-core";
@@ -86,11 +85,10 @@ export async function runWalletDblessTest(t: GlobalTestState) {
console.log(exchangeInfo);
await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
-
- const defaultConfig = applyRunConfigDefaults();
+ const denomselAllowLate = false;
const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8" as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
});
const coin = await withdrawCoin({
@@ -133,10 +131,10 @@ export async function runWalletDblessTest(t: GlobalTestState) {
const refreshDenoms = [
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
}),
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, {
- denomselAllowLate: defaultConfig.testing.denomselAllowLate,
+ denomselAllowLate,
}),
];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts
index ba2b2670c..7378e272a 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts
@@ -34,7 +34,7 @@ import {
GlobalTestState,
WalletClient,
WalletService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import { withdrawViaBankV3 } from "../harness/helpers.js";
@@ -64,7 +64,7 @@ export async function runWalletDd48Test(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
index 3a1b467c3..3d7ea32e6 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
@@ -32,7 +32,7 @@ import {
FakebankService,
GlobalTestState,
HarnessExchangeBankAccount,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -98,7 +98,7 @@ export async function runWalletExchangeUpdateTest(
).href,
accountName: "myexchange",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"),
};
await exchangeOne.addBankAccount("1", exchangeBankAccount);
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
index 4062e186d..3b1f4bf27 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts
@@ -20,39 +20,82 @@
import {
AmountString,
Duration,
+ j2s,
PaymentInsufficientBalanceDetails,
TalerErrorCode,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
GlobalTestState,
- generateRandomPayto,
+ HarnessExchangeBankAccount,
setupDb,
} from "../harness/harness.js";
-import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "../harness/helpers.js";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
// Set up test environment
- const db = await setupDb(t);
-
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
- let {
- bankClient,
- exchange,
- merchant,
- walletService,
- walletClient,
- } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {
- skipWireFeeCreation: true,
+ let { bankClient, bank, exchange, merchant, walletClient } =
+ await createSimpleTestkudosEnvironmentV3(t, coinConfig, {
+ skipWireFeeCreation: true,
+ });
+
+ const dbTwo = await setupDb(t, {
+ nameSuffix: "two",
});
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 9081,
+ database: dbTwo.connStr,
+ });
+
+ {
+ const receiverName = "Exchange2";
+ const exchangeBankUsername = "exchange2";
+ const exchangeBankPassword = "mypw";
+ const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+ const wireGatewayApiBaseUrl = new URL(
+ `accounts/${exchangeBankUsername}/taler-wire-gateway/`,
+ bank.corebankApiBaseUrl,
+ ).href;
+
+ const exchangeBankAccount: HarnessExchangeBankAccount = {
+ wireGatewayApiBaseUrl,
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ accountPaytoUri: exchangePaytoUri,
+ skipWireFeeCreation: true,
+ };
+
+ await exchangeTwo.addBankAccount("1", exchangeBankAccount);
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchangeTwo.addCoinConfigList(coinConfig);
+
+ await exchangeTwo.start();
+ }
+
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -61,7 +104,7 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -75,6 +118,8 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
},
});
+ t.logStep("setup-done");
+
const wres = await withdrawViaBankV3(t, {
amount: "TESTKUDOS:10",
bankClient,
@@ -83,29 +128,74 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) {
});
await wres.withdrawalFinishedCond;
- const exc = await t.assertThrowsTalerErrorAsync(async () => {
- await walletClient.call(WalletApiOperation.PrepareDeposit, {
- amount: "TESTKUDOS:5" as AmountString,
- depositPaytoUri: "payto://x-taler-bank/localhost/foobar",
+ {
+ const exc = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.CheckDeposit, {
+ amount: "TESTKUDOS:5" as AmountString,
+ depositPaytoUri: "payto://x-taler-bank/localhost/foobar",
+ });
+ });
+
+ t.assertDeepEqual(
+ exc.errorDetail.code,
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ );
+
+ const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
+ exc.errorDetail.insufficientBalanceDetails;
+
+ t.assertAmountEquals(
+ insufficientBalanceDetails.balanceAvailable,
+ "TESTKUDOS:9.72",
+ );
+ t.assertAmountEquals(
+ insufficientBalanceDetails.balanceExchangeDepositable,
+ "TESTKUDOS:0",
+ );
+ }
+
+ t.logStep("start-p2p-push-test");
+
+ // Now check for p2p-push
+
+ {
+ const wres2 = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:5",
+ bankClient,
+ exchange: exchangeTwo,
+ walletClient,
+ });
+ await wres2.withdrawalFinishedCond;
+
+ const exc = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: "TESTKUDOS:20" as AmountString,
+ });
});
- });
- t.assertDeepEqual(
- exc.errorDetail.code,
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- );
-
- const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
- exc.errorDetail.insufficientBalanceDetails;
-
- t.assertAmountEquals(
- insufficientBalanceDetails.balanceAvailable,
- "TESTKUDOS:9.72",
- );
- t.assertAmountEquals(
- insufficientBalanceDetails.balanceExchangeDepositable,
- "TESTKUDOS:0",
- );
+ const insufficientBalanceDetails: PaymentInsufficientBalanceDetails =
+ exc.errorDetail.insufficientBalanceDetails;
+
+ const perMyExchange =
+ insufficientBalanceDetails.perExchange[exchange.baseUrl];
+
+ t.assertTrue(!!perMyExchange);
+
+ console.log(j2s(exc.errorDetail));
+
+ t.assertAmountEquals(
+ insufficientBalanceDetails.amountRequested,
+ "TESTKUDOS:20",
+ );
+ t.assertAmountEquals(
+ insufficientBalanceDetails.maxEffectiveSpendAmount,
+ "TESTKUDOS:14.22",
+ );
+ t.assertAmountEquals(
+ perMyExchange.maxEffectiveSpendAmount,
+ "TESTKUDOS:9.47",
+ );
+ }
}
runWalletInsufficientBalanceTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts b/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts
new file mode 100644
index 000000000..ba0038608
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts
@@ -0,0 +1,96 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ NotificationType,
+ ObservabilityEventType,
+ TransactionMajorState,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation, parseTransactionIdentifier } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, getTestHarnessPaytoForLabel } from "harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "harness/helpers.js";
+import { TaskRunResultType } from "../../../taler-wallet-core/src/common.js";
+
+/**
+ * Run test for hintNetworkAvailability in wallet-core
+ */
+export async function runWalletNetworkAvailabilityTest(t: GlobalTestState) {
+
+ // Set up test environment
+ const { bankClient, walletClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ // We need this to listen to the network-required observability event
+ walletTestObservability: true,
+ });
+
+ await withdrawViaBankV3(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const networkRequiredCond = walletClient.waitForNotificationCond((x) => {
+ return (x.type === NotificationType.TaskObservabilityEvent
+ && x.event.type === ObservabilityEventType.ShepherdTaskResult
+ && x.event.resultType === TaskRunResultType.NetworkRequired
+ );
+ });
+
+ const refreshCreatedCond = walletClient.waitForNotificationCond((x) => {
+ return (x.type === NotificationType.TransactionStateTransition &&
+ parseTransactionIdentifier(x.transactionId)?.tag === TransactionType.Refresh
+ );
+ });
+
+ const refreshDoneCond = walletClient.waitForNotificationCond((x) => {
+ return (x.type === NotificationType.TransactionStateTransition &&
+ parseTransactionIdentifier(x.transactionId)?.tag === TransactionType.Refresh
+ && x.newTxState.major === TransactionMajorState.Done
+ );
+ });
+
+ await walletClient.call(WalletApiOperation.HintNetworkAvailability, {
+ isNetworkAvailable: false,
+ });
+
+ const depositGroupResult = await walletClient.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10.5" as AmountString,
+ depositPaytoUri: getTestHarnessPaytoForLabel("foo"),
+ },
+ );
+
+ // refresh should not continue due to network-required
+ await networkRequiredCond;
+
+ await walletClient.call(WalletApiOperation.HintNetworkAvailability, {
+ isNetworkAvailable: true,
+ });
+
+ await refreshCreatedCond;
+
+ // refresh should finish due to network being restored
+ await refreshDoneCond;
+}
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
index 5088c8228..27280103b 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
@@ -32,7 +32,7 @@ import {
MerchantService,
WalletClient,
WalletService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
generateRandomTestIban,
setupDb,
} from "../harness/harness.js";
@@ -62,7 +62,7 @@ export async function runWalletNotificationsTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
const merchant = await MerchantService.create(t, {
name: "testmerchant-1",
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts
index 55a60cb76..cf924f649 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts
@@ -26,7 +26,7 @@ import {
GlobalTestState,
WalletClient,
WalletService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import { withdrawViaBankV3 } from "../harness/helpers.js";
@@ -53,7 +53,7 @@ export async function runWalletObservabilityTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
index 0f1efd35e..7b101bc18 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
@@ -80,7 +80,7 @@ export async function runWalletRefreshErrorsTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.ForceRefresh, {
refreshCoinSpecs: [
{
- coinPub: coinDump.coins[0].coin_pub,
+ coinPub: coinDump.coins[0].coinPub,
amount: "TESTKUDOS:3" as AmountString,
},
],
@@ -95,7 +95,7 @@ export async function runWalletRefreshErrorsTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.ForceRefresh, {
refreshCoinSpecs: [
{
- coinPub: coinDump.coins[0].coin_pub,
+ coinPub: coinDump.coins[0].coinPub,
amount: "TESTKUDOS:3" as AmountString,
},
],
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
index 93fe94270..6197e6c36 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts
@@ -30,7 +30,7 @@ import {
WalletApiOperation,
parseTransactionIdentifier,
} from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV3,
makeTestPaymentV2,
@@ -120,7 +120,7 @@ export async function runWalletRefreshTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10.5" as AmountString,
- depositPaytoUri: generateRandomPayto("foo"),
+ depositPaytoUri: getTestHarnessPaytoForLabel("foo"),
},
);
@@ -175,7 +175,7 @@ export async function runWalletRefreshTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10.5" as AmountString,
- depositPaytoUri: generateRandomPayto("foo"),
+ depositPaytoUri: getTestHarnessPaytoForLabel("foo"),
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts
index c5a0fd363..359adc7e3 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts
@@ -32,7 +32,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import {
@@ -74,7 +74,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
@@ -121,7 +121,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -130,7 +130,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
index 001081532..42b73654c 100644
--- a/packages/taler-harness/src/integrationtests/test-wallettesting.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
@@ -25,16 +25,10 @@
import { AmountString, Amounts, CoinStatus } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState, setupDb } from "../harness/harness.js";
import {
- ExchangeService,
- GlobalTestState,
- MerchantService,
- setupDb,
- generateRandomPayto,
- FakebankService,
-} from "../harness/harness.js";
-import {
- SimpleTestEnvironmentNg,
+ SimpleTestEnvironmentNg3,
+ createSimpleTestkudosEnvironmentV3,
createWalletDaemonWithClient,
} from "../harness/helpers.js";
@@ -47,57 +41,11 @@ const merchantAuthToken = "secret-token:sandbox";
export async function createMyEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
-): Promise<SimpleTestEnvironmentNg> {
+): Promise<SimpleTestEnvironmentNg3> {
const db = await setupDb(t);
- const bank = await FakebankService.create(t, {
- allowRegistrations: true,
- currency: "TESTKUDOS",
- database: db.connStr,
- httpPort: 8082,
- });
-
- const exchange = ExchangeService.create(t, {
- name: "testexchange-1",
- currency: "TESTKUDOS",
- httpPort: 8081,
- database: db.connStr,
- });
-
- const merchant = await MerchantService.create(t, {
- name: "testmerchant-1",
- currency: "TESTKUDOS",
- httpPort: 8083,
- database: db.connStr,
- });
-
- const exchangeBankAccount = await bank.createExchangeAccount(
- "myexchange",
- "x",
- );
- exchange.addBankAccount("1", exchangeBankAccount);
-
- bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
-
- await bank.start();
-
- await bank.pingUntilAvailable();
-
- exchange.addCoinConfigList(coinConfig);
-
- await exchange.start();
- await exchange.pingUntilAvailable();
-
- merchant.addExchange(exchange);
-
- await merchant.start();
- await merchant.pingUntilAvailable();
-
- await merchant.addInstanceWithWireAccount({
- id: "default",
- name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
- });
+ const { bankClient, bank, exchange, merchant, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironmentV3(t, coinConfig, {});
console.log("setup done!");
@@ -115,6 +63,7 @@ export async function createMyEnvironment(
walletClient,
walletService,
bank,
+ bankClient,
exchangeBankAccount,
};
}
@@ -123,13 +72,13 @@ export async function createMyEnvironment(
* Run test for basic, bank-integrated withdrawal.
*/
export async function runWallettestingTest(t: GlobalTestState) {
- const { walletClient, bank, exchange, merchant } =
+ const { walletClient, bankClient, exchange, merchant } =
await createMyEnvironment(t);
await walletClient.call(WalletApiOperation.RunIntegrationTest, {
amountToSpend: "TESTKUDOS:5" as AmountString,
amountToWithdraw: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ corebankApiBaseUrl: bankClient.baseUrl,
exchangeBaseUrl: exchange.baseUrl,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
@@ -152,7 +101,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
amount: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ corebankApiBaseUrl: bankClient.baseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
@@ -177,7 +126,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
amount: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ corebankApiBaseUrl: bankClient.baseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
@@ -191,10 +140,10 @@ export async function runWallettestingTest(t: GlobalTestState) {
{
for (const c of coinDump.coins) {
if (
- c.coin_status === CoinStatus.Fresh &&
- 0 === Amounts.cmp(c.denom_value, "TESTKUDOS:8")
+ c.coinStatus === CoinStatus.Fresh &&
+ 0 === Amounts.cmp(c.denomValue, "TESTKUDOS:8")
) {
- susp = c.coin_pub;
+ susp = c.coinPub;
}
}
}
@@ -235,9 +184,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.ClearDb, {});
await walletClient.call(WalletApiOperation.RunIntegrationTestV2, {
- amountToSpend: "TESTKUDOS:5" as AmountString,
- amountToWithdraw: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ corebankApiBaseUrl: bankClient.baseUrl,
exchangeBaseUrl: exchange.baseUrl,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
index c55e1faf0..2f6d2ff5d 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts
@@ -33,13 +33,12 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
- BankService,
+ BankService,
ExchangeService,
- FakebankService,
GlobalTestState,
HarnessExchangeBankAccount,
MerchantService,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
import { createWalletDaemonWithClient } from "../harness/helpers.js";
@@ -104,7 +103,7 @@ async function runTestfakeConversionService(): Promise<TestfakeConversionService
cashout_rounding_mode: "zero",
cashout_tiny_amount: "A:1" as AmountString,
},
- } satisfies TalerBankConversionApi.IntegrationConfig),
+ } satisfies TalerBankConversionApi.TalerConversionInfoConfig),
);
} else if (path === "/cashin-rate") {
res.writeHead(200, { "Content-Type": "application/json" });
@@ -165,7 +164,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
).href,
accountName: "myexchange",
accountPassword: "x",
- accountPaytoUri: generateRandomPayto("myexchange"),
+ accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"),
conversionUrl: "http://localhost:8071/",
};
@@ -204,7 +203,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [generateRandomPayto("merchant-default")],
+ paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -213,16 +212,15 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [generateRandomPayto("minst1")],
+ paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
});
- const { walletClient } = await createWalletDaemonWithClient(
- t,
- { name: "wallet" },
- );
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ });
await runTestfakeConversionService();
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts
new file mode 100644
index 000000000..3cd02882b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts
@@ -0,0 +1,101 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js";
+
+/**
+ * Test for a withdrawal that is externally confirmed.
+ */
+export async function runWithdrawalExternalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ // Create a withdrawal operation
+
+ const bankUser = await bankClient.createRandomBankUser();
+ bankClient.setAuth(bankUser);
+ const wop = await bankClient.createWithdrawalOperation(
+ bankUser.username,
+ "TESTKUDOS:10",
+ );
+
+ const talerWithdrawUri = wop.taler_withdraw_uri + "?external-confirmation=1";
+
+ // Hand it to the wallet
+
+ const detResp = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: talerWithdrawUri,
+ },
+ );
+
+ const acceptResp = await walletClient.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: detResp.defaultExchangeBaseUrl!!,
+ talerWithdrawUri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: acceptResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: acceptResp.transactionId,
+ },
+ );
+
+ // Now we check that the external-confirmation=1 flag actually did something!
+
+ t.assertDeepEqual(txDetails.type, TransactionType.Withdrawal);
+ t.assertDeepEqual(
+ txDetails.withdrawalDetails.type,
+ WithdrawalType.TalerBankIntegrationApi,
+ );
+ t.assertDeepEqual(txDetails.withdrawalDetails.externalConfirmation, true);
+ t.assertDeepEqual(txDetails.withdrawalDetails.bankConfirmationUrl, undefined);
+
+ t.logStep("confirming withdrawal operation");
+
+ await bankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWithdrawalExternalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
index 0657d2da7..c7bf0b938 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
@@ -25,7 +25,7 @@ import {
ExchangeService,
GlobalTestState,
WalletCli,
- generateRandomPayto,
+ getTestHarnessPaytoForLabel,
setupDb,
} from "../harness/harness.js";
@@ -85,7 +85,7 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
- let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+ let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
index ffc7249b8..7a5aa8bfd 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts
@@ -49,22 +49,25 @@ export async function runWithdrawalFlexTest(t: GlobalTestState) {
console.log(j2s(r1));
+ t.assertTrue(!r1.amount);
+
// Withdraw
- const r2 = await walletClient.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: wop.taler_withdraw_uri,
- amount: "TESTKUDOS:10",
- },
- );
+ await walletClient.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ amount: "TESTKUDOS:10",
+ });
await bankClient.confirmWithdrawalOperation(user.username, {
withdrawalOperationId: wop.withdrawal_id,
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:9.72");
}
runWithdrawalFlexTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts
new file mode 100644
index 000000000..0daa53f64
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts
@@ -0,0 +1,172 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AgeRestriction,
+ Amounts,
+ AmountString,
+ codecForExchangeWithdrawBatchResponse,
+ encodeCrock,
+ ExchangeBatchWithdrawRequest,
+ getRandomBytes,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
+import {
+ CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+ TalerCryptoInterface,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ checkReserve,
+ CoinInfo,
+ downloadExchangeInfo,
+ findDenomOrThrow,
+ ReserveKeypair,
+ topupReserveWithBank,
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { DenominationRecord } from "../../../taler-wallet-core/src/db.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runWithdrawalIdempotentTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t);
+
+ const http = harnessHttpLib;
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+
+ const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
+
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchange.baseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ http,
+ reservePub: reserveKeyPair.pub,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeInfo,
+ });
+
+ console.log("waiting for longpoll request");
+ const resp = await longpollReq;
+ console.log(`got response, status ${resp.status}`);
+
+ console.log(exchangeInfo);
+
+ await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
+ const denomselAllowLate = false;
+
+ const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8" as AmountString, {
+ denomselAllowLate,
+ });
+
+ await myWithdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ },
+ denom: d1,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+}
+
+async function myWithdrawCoin(args: {
+ http: HttpRequestLibrary;
+ cryptoApi: TalerCryptoInterface;
+ reserveKeyPair: ReserveKeypair;
+ denom: DenominationRecord;
+ exchangeBaseUrl: string;
+}): Promise<CoinInfo> {
+ const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args;
+ const planchet = await cryptoApi.createPlanchet({
+ coinIndex: 0,
+ denomPub: denom.denomPub,
+ feeWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
+ reservePriv: reserveKeyPair.reservePriv,
+ reservePub: reserveKeyPair.reservePub,
+ secretSeed: encodeCrock(getRandomBytes(32)),
+ value: Amounts.parseOrThrow(denom.value),
+ });
+
+ const reqBody: ExchangeBatchWithdrawRequest = {
+ planchets: [
+ {
+ denom_pub_hash: planchet.denomPubHash,
+ reserve_sig: planchet.withdrawSig,
+ coin_ev: planchet.coinEv,
+ },
+ ],
+ };
+ const reqUrl = new URL(
+ `reserves/${planchet.reservePub}/batch-withdraw`,
+ exchangeBaseUrl,
+ ).href;
+
+ const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody });
+ const rBatch = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeWithdrawBatchResponse(),
+ );
+
+ {
+ // Check for idempotency!
+ const resp2 = await http.fetch(reqUrl, { method: "POST", body: reqBody });
+ await readSuccessResponseJsonOrThrow(
+ resp2,
+ codecForExchangeWithdrawBatchResponse(),
+ );
+ }
+
+ const ubSig = await cryptoApi.unblindDenominationSignature({
+ planchet,
+ evSig: rBatch.ev_sigs[0].ev_sig,
+ });
+
+ return {
+ coinPriv: planchet.coinPriv,
+ coinPub: planchet.coinPub,
+ denomSig: ubSig,
+ denomPub: denom.denomPub,
+ denomPubHash: denom.denomPubHash,
+ feeDeposit: Amounts.stringify(denom.fees.feeDeposit),
+ feeRefresh: Amounts.stringify(denom.fees.feeRefresh),
+ exchangeBaseUrl: args.exchangeBaseUrl,
+ maxAge: AgeRestriction.AGE_UNRESTRICTED,
+ };
+}
+
+runWithdrawalIdempotentTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts
new file mode 100644
index 000000000..b5c6cc769
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts
@@ -0,0 +1,78 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { j2s, Logger } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js";
+
+const logger = new Logger("test-withdrawal-prepare.ts");
+
+/**
+ * Test the separate prepare step of a bank-integrated withdrawal.
+ */
+export async function runWithdrawalPrepareTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t);
+
+ // Create a withdrawal operation
+ const user = await bankClient.createRandomBankUser();
+ bankClient.setAuth(user);
+ const wop = await bankClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:20",
+ );
+
+ const r1 = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(j2s(r1));
+
+ //t.assertTrue(!r1.amount);
+
+ // Withdraw
+
+ const prepRes = await walletClient.call(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ logger.info(`Prep res: ${j2s(prepRes)}`);
+
+ // Make sure that we can get the transaction details for the prepared transaction
+
+ const txDetail = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: prepRes.transactionId,
+ },
+ );
+
+ logger.info(`Transaction details: ${j2s(txDetail)}`);
+}
+
+runWithdrawalPrepareTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 4588310b1..5b36d6145 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -28,6 +28,7 @@ import {
shouldLingerInTest,
} from "../harness/harness.js";
import { getSharedTestDir } from "../harness/helpers.js";
+import { runAccountRestrictionsTest } from "./test-account-restrictions.js";
import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js";
import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js";
import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js";
@@ -42,12 +43,26 @@ import { runDepositTest } from "./test-deposit.js";
import { runExchangeDepositTest } from "./test-exchange-deposit.js";
import { runExchangeManagementFaultTest } from "./test-exchange-management-fault.js";
import { runExchangeManagementTest } from "./test-exchange-management.js";
+import { runExchangeMasterPubChangeTest } from "./test-exchange-master-pub-change.js";
import { runExchangePurseTest } from "./test-exchange-purse.js";
import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runFeeRegressionTest } from "./test-fee-regression.js";
import { runForcedSelectionTest } from "./test-forced-selection.js";
+import { runKycBalanceWithdrawalTest } from "./test-kyc-balance-withdrawal.js";
+import { runKycDepositAggregateTest } from "./test-kyc-deposit-aggregate.js";
+import { runKycDepositDepositKyctransferTest } from "./test-kyc-deposit-deposit-kyctransfer.js";
+import { runKycDepositDepositTest } from "./test-kyc-deposit-deposit.js";
+import { runKycExchangeWalletTest } from "./test-kyc-exchange-wallet.js";
+import { runKycFormWithdrawalTest } from "./test-kyc-form-withdrawal.js";
+import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js";
+import { runKycMerchantDepositTest } from "./test-kyc-merchant-deposit.js";
+import { runKycNewMeasureTest } from "./test-kyc-new-measure.js";
+import { runKycPeerPullTest } from "./test-kyc-peer-pull.js";
+import { runKycPeerPushTest } from "./test-kyc-peer-push.js";
+import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js";
import { runKycTest } from "./test-kyc.js";
import { runLibeufinBankTest } from "./test-libeufin-bank.js";
+import { runMerchantCategoriesTest } from "./test-merchant-categories.js";
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js";
import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js";
import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js";
@@ -81,6 +96,7 @@ import { runRefundAutoTest } from "./test-refund-auto.js";
import { runRefundGoneTest } from "./test-refund-gone.js";
import { runRefundIncrementalTest } from "./test-refund-incremental.js";
import { runRefundTest } from "./test-refund.js";
+import { runRepurchaseTest } from "./test-repurchase.js";
import { runRevocationTest } from "./test-revocation.js";
import { runSimplePaymentTest } from "./test-simple-payment.js";
import { runStoredBackupsTest } from "./test-stored-backups.js";
@@ -106,6 +122,7 @@ import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js";
import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js";
import { runWalletGenDbTest } from "./test-wallet-gendb.js";
import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js";
+import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js";
import { runWalletNotificationsTest } from "./test-wallet-notifications.js";
import { runWalletObservabilityTest } from "./test-wallet-observability.js";
import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js";
@@ -116,12 +133,15 @@ import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
+import { runWithdrawalExternalTest } from "./test-withdrawal-external.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js";
import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
+import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
+import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js";
/**
* Test runner.
@@ -229,11 +249,31 @@ const allTests: TestMainFunction[] = [
runWalletBlockedPayPeerPullTest,
runWalletExchangeUpdateTest,
runWalletRefreshErrorsTest,
+ runWalletNetworkAvailabilityTest,
runPeerPullLargeTest,
runPeerPushLargeTest,
runWithdrawalHandoverTest,
runWithdrawalAmountTest,
runWithdrawalFlexTest,
+ runExchangeMasterPubChangeTest,
+ runMerchantCategoriesTest,
+ runWithdrawalExternalTest,
+ runWithdrawalIdempotentTest,
+ runKycThresholdWithdrawalTest,
+ runKycExchangeWalletTest,
+ runKycPeerPushTest,
+ runKycPeerPullTest,
+ runKycDepositAggregateTest,
+ runKycFormWithdrawalTest,
+ runKycBalanceWithdrawalTest,
+ runKycNewMeasureTest,
+ runKycDepositDepositTest,
+ runKycMerchantDepositTest,
+ runKycMerchantAggregateTest,
+ runKycDepositDepositKyctransferTest,
+ runWithdrawalPrepareTest,
+ runAccountRestrictionsTest,
+ runRepurchaseTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 87e6a7cfa..697688946 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
index f58757fb5..59cd3ab6d 100644
--- a/packages/taler-util/src/MerchantApiClient.ts
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -15,12 +15,6 @@
*/
import { codecForAny } from "./codec.js";
-import {
- TalerMerchantApi,
- codecForMerchantConfig,
- codecForMerchantOrderPrivateStatusResponse,
- codecForPostOrderResponse,
-} from "./http-client/types.js";
import { HttpStatusCode } from "./http-status-codes.js";
import {
createPlatformHttpLib,
@@ -28,7 +22,6 @@ import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "./http.js";
-import { FacadeCredentials } from "./libeufin-api-types.js";
import { LibtoolVersion } from "./libtool-version.js";
import { Logger } from "./logging.js";
import {
@@ -41,8 +34,16 @@ import {
opSuccessFromHttp,
opUnknownFailure,
} from "./operation.js";
-import { AmountString } from "./taler-types.js";
import { TalerProtocolDuration } from "./time.js";
+import { AmountString } from "./types-taler-common.js";
+import {
+ OtpDeviceAddDetails,
+ codecForTalerMerchantConfigResponse,
+ codecForMerchantOrderPrivateStatusResponse,
+ codecForPostOrderResponse,
+} from "./types-taler-merchant.js";
+
+import * as TalerMerchantApi from "./types-taler-merchant.js";
const logger = new Logger("MerchantApiClient.ts");
@@ -87,21 +88,6 @@ export interface DeleteTippingReserveArgs {
purge?: boolean;
}
-interface MerchantBankAccount {
- // The payto:// URI where the wallet will send coins.
- payto_uri: string;
-
- // Optional base URL for a facade where the
- // merchant backend can see incoming wire
- // transfers to reconcile its accounting
- // with that of the exchange. Used by
- // taler-merchant-wirewatch.
- credit_facade_url?: string;
-
- // Credentials for accessing the credit facade.
- credit_facade_credentials?: FacadeCredentials;
-}
-
export interface MerchantInstanceConfig {
auth: MerchantAuthConfiguration;
id: string;
@@ -119,25 +105,10 @@ export interface PrivateOrderStatusQuery {
sessionId?: string;
}
-export 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: number;
-
- // Counter for counter-based OTP devices.
- otp_ctr?: number;
-}
-
/**
* Client for the GNU Taler merchant backend.
+ *
+ * @deprecated in favor of TalerMerchantInstanceHttpClient
*/
export class MerchantApiClient {
/**
@@ -325,14 +296,14 @@ export class MerchantApiClient {
* https://docs.taler.net/core/api-merchant.html#get--config
*
*/
- async getConfig(): Promise<OperationOk<TalerMerchantApi.VersionResponse>> {
+ async getConfig(): Promise<OperationOk<TalerMerchantApi.TalerMerchantConfigResponse>> {
const url = new URL(`config`, this.baseUrl);
const resp = await this.httpClient.fetch(url.href, {
method: "GET",
});
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForMerchantConfig());
+ return opSuccessFromHttp(resp, codecForTalerMerchantConfigResponse());
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
index 3a30755ce..325dae68d 100644
--- a/packages/taler-util/src/ReserveStatus.ts
+++ b/packages/taler-util/src/ReserveStatus.ts
@@ -23,7 +23,7 @@
*/
import { codecForAmountString } from "./amounts.js";
import { Codec, buildCodecForObject } from "./codec.js";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
/**
* Status of a reserve.
diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts
index 7a3c69d07..0237ad724 100644
--- a/packages/taler-util/src/ReserveTransaction.ts
+++ b/packages/taler-util/src/ReserveTransaction.ts
@@ -38,7 +38,7 @@ import {
EddsaSignatureString,
EddsaPublicKeyString,
CoinPublicKeyString,
-} from "./taler-types.js";
+} from "./types-taler-common.js";
import {
AbsoluteTime,
codecForTimestamp,
diff --git a/packages/taler-util/src/account-restrictions.ts b/packages/taler-util/src/account-restrictions.ts
new file mode 100644
index 000000000..3b3724487
--- /dev/null
+++ b/packages/taler-util/src/account-restrictions.ts
@@ -0,0 +1,43 @@
+/*
+ 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/>
+ */
+
+import { InternationalizedString } from "./types-taler-common.js";
+import { AccountRestriction } from "./types-taler-exchange.js";
+
+export function checkAccountRestriction(
+ paytoUri: string,
+ restrictions: AccountRestriction[],
+): { ok: boolean; hint?: string; hintI18n?: InternationalizedString } {
+ for (const myRestriction of restrictions) {
+ switch (myRestriction.type) {
+ case "deny":
+ return { ok: false };
+ case "regex": {
+ const regex = new RegExp(myRestriction.payto_regex);
+ if (!regex.test(paytoUri)) {
+ return {
+ ok: false,
+ hint: myRestriction.human_hint,
+ hintI18n: myRestriction.human_hint_i18n,
+ };
+ }
+ }
+ }
+ }
+ return {
+ ok: true,
+ };
+}
diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts
index 449a6319a..d1be17824 100644
--- a/packages/taler-util/src/amounts.test.ts
+++ b/packages/taler-util/src/amounts.test.ts
@@ -17,7 +17,7 @@
import test from "ava";
import { Amounts, AmountJson, amountMaxValue } from "./amounts.js";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
const jAmt = (
value: number,
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
index 82a3d3b68..e173e8b96 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -31,7 +31,7 @@ import {
renderContext,
} from "./codec.js";
import { CurrencySpecification } from "./index.js";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
/**
* Number of fractional units that one value unit represents.
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
index e1409087f..67cde0c18 100644
--- a/packages/taler-util/src/bank-api-client.ts
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -22,6 +22,7 @@
* Imports.
*/
import {
+ AccountData,
AmountString,
base64FromArrayBuffer,
buildCodecForObject,
@@ -36,6 +37,7 @@ import {
opEmptySuccess,
opKnownHttpFailure,
opUnknownFailure,
+ RegisterAccountRequest,
stringToBytes,
TalerError,
TalerErrorCode,
@@ -46,7 +48,6 @@ import {
expectSuccessResponseOrThrow,
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
- readSuccessResponseTextOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
@@ -150,14 +151,25 @@ export class WireGatewayApiClient {
logger.info(`add-incoming response status: ${resp.status}`);
await checkSuccessResponseOrThrow(resp);
}
-}
-
-export interface ChallengeContactData {
- // E-Mail address
- email?: string;
- // Phone number.
- phone?: string;
+ async adminAddKycauth(params: {
+ amount: string;
+ accountPub: string;
+ debitAccountPayto: string;
+ }): Promise<void> {
+ let url = new URL(`admin/add-kycauth`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ amount: params.amount,
+ account_pub: params.accountPub,
+ debit_account: params.debitAccountPayto,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ logger.info(`add-kycauth response status: ${resp.status}`);
+ await checkSuccessResponseOrThrow(resp);
+ }
}
export interface AccountBalance {
@@ -165,70 +177,6 @@ export interface AccountBalance {
credit_debit_indicator: "credit" | "debit";
}
-export interface RegisterAccountRequest {
- // Username
- username: string;
-
- // Password.
- password: string;
-
- // Legal name of the account owner
- name: string;
-
- // Defaults to false.
- is_public?: boolean;
-
- // Is this a taler exchange account?
- // If true:
- // - incoming transactions to the account that do not
- // have a valid reserve public key are automatically
- // - the account provides the taler-wire-gateway-api endpoints
- // Defaults to false.
- is_taler_exchange?: boolean;
-
- // Addresses where to send the TAN for transactions.
- // Currently only used for cashouts.
- // If missing, cashouts will fail.
- // In the future, might be used for other transactions
- // as well.
- challenge_contact_data?: ChallengeContactData;
-
- // 'payto' address pointing a bank account
- // external to the libeufin-bank.
- // Payments will be sent to this bank account
- // when the user wants to convert the local currency
- // back to fiat currency outside libeufin-bank.
- cashout_payto_uri?: string;
-
- // Internal payto URI of this bank account.
- // Used mostly for testing.
- payto_uri?: string;
-}
-
-export interface AccountData {
- // Legal name of the account owner.
- name: string;
-
- // Available balance on the account.
- balance: AccountBalance;
-
- // payto://-URI of the account.
- payto_uri: string;
-
- // Number indicating the max debit allowed for the requesting user.
- debit_threshold: AmountString;
-
- contact_data?: ChallengeContactData;
-
- // 'payto' address pointing the bank account
- // where to send cashouts. This field is optional
- // because not all the accounts are required to participate
- // in the merchants' circuit. One example is the exchange:
- // that never cashouts. Registering these accounts can
- // be done via the access API.
- cashout_payto_uri?: string;
-}
-
export interface ConfirmWithdrawalArgs {
withdrawalOperationId: string;
}
@@ -286,21 +234,6 @@ export class TalerCorebankApiClient {
logger.info(`result: ${j2s(res)}`);
}
- async createTransaction(
- username: string,
- req: BankAccessApiCreateTransactionRequest,
- ): Promise<any> {
- const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
-
- const resp = await this.httpLib.fetch(reqUrl.href, {
- method: "POST",
- body: req,
- headers: this.makeAuthHeader(),
- });
-
- return await readSuccessResponseJsonOrThrow(resp, codecForAny());
- }
-
async registerAccountExtended(req: RegisterAccountRequest): Promise<void> {
const url = new URL("accounts", this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts
index 60969af69..914e0cd18 100644
--- a/packages/taler-util/src/clk.ts
+++ b/packages/taler-util/src/clk.ts
@@ -23,7 +23,7 @@ import {
readlinePrompt,
pathBasename,
} from "#compat-impl";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
export namespace clk {
class Converter<T> {}
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
index 9378d25e8..ed0f0db65 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -35,6 +35,9 @@ import {
type empty = Record<string, never>;
export interface DetailsMap {
+ [TalerErrorCode.WALLET_PAY_MERCHANT_KYC_MISSING]: {
+ exchangeResponse: any;
+ };
[TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
innerError: TalerErrorDetail;
transactionId?: string;
@@ -107,6 +110,10 @@ export interface DetailsMap {
requestUrl: string;
requestMethod: string;
httpStatusCode: number;
+ /**
+ * Original response which is malformed
+ */
+ response?: string;
validationError?: string;
/**
* Content type of the response, usually only specified if not the
@@ -166,17 +173,28 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
innerError: TalerErrorDetail | undefined;
};
+ [TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED]: {
+ exchangeBaseUrl: string;
+ tosStatus: string;
+ currentEtag: string | undefined;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT]: {
+ detail?: string;
+ };
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
+export type TypedTalerErrorDetail<Y> = TalerErrorDetail &
+ (Y extends keyof DetailsMap ? DetailsMap[Y] : {});
+
export function makeErrorDetail<C extends TalerErrorCode>(
code: C,
detail: ErrBody<C>,
hint?: string,
): TalerErrorDetail {
if (!hint && !(detail as any).hint) {
- hint = getDefaultHint(code);
+ hint = getDefaultTalerErrorHint(code);
}
const when = AbsoluteTime.now();
return { code, when, hint, ...detail };
@@ -198,7 +216,7 @@ export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
return `Error (${ed.code}/${errName})`;
}
-function getDefaultHint(code: number): string {
+export function getDefaultTalerErrorHint(code: number): string {
const errName = TalerErrorCode[code];
if (errName) {
return `Error (${errName})`;
@@ -235,6 +253,21 @@ type TalerHttpErrorsDetails = {
export type TalerHttpError =
TalerHttpErrorsDetails[keyof TalerHttpErrorsDetails];
+/**
+ * Construct typed error details.
+ * Fills in the hint with a default based on the error code name.
+ */
+export function makeTalerErrorDetail<C extends TalerErrorCode>(
+ code: C,
+ errBody: ErrBody<C>,
+ hint?: string,
+): TalerErrorDetail {
+ if (!hint) {
+ hint = getDefaultTalerErrorHint(code);
+ }
+ return { code, hint, ...errBody };
+}
+
export class TalerError<T = any> extends Error {
errorDetail: TalerErrorDetail & T;
cause: Error | undefined;
@@ -252,7 +285,7 @@ export class TalerError<T = any> extends Error {
cause?: Error,
): TalerError {
if (!hint) {
- hint = getDefaultHint(code);
+ hint = getDefaultTalerErrorHint(code);
}
const when = AbsoluteTime.now();
return new TalerError<unknown>({ code, when, hint, ...detail }, cause);
diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts
index 8897a2fa0..fa48f3da6 100644
--- a/packages/taler-util/src/http-client/authentication.ts
+++ b/packages/taler-util/src/http-client/authentication.ts
@@ -33,10 +33,10 @@ import {
} from "../operation.js";
import {
AccessToken,
- TalerAuthentication,
+ TokenRequest,
codecForTokenSuccessResponse,
codecForTokenSuccessResponseMerchant,
-} from "./types.js";
+} from "../types-taler-common.js";
import { makeBearerTokenAuthHeader } from "./utils.js";
export class TalerAuthenticationHttpClient {
@@ -64,7 +64,7 @@ export class TalerAuthenticationHttpClient {
async createAccessTokenBasic(
username: string,
password: string,
- body: TalerAuthentication.TokenRequest,
+ body: TokenRequest,
) {
const url = new URL(`token`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -91,10 +91,7 @@ export class TalerAuthenticationHttpClient {
*
* @returns
*/
- async createAccessTokenBearer(
- token: AccessToken,
- body: TalerAuthentication.TokenRequest,
- ) {
+ async createAccessTokenBearer(token: AccessToken, body: TokenRequest) {
const url = new URL(`token`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
index cb14d8b34..5001b9de5 100644
--- a/packages/taler-util/src/http-client/bank-conversion.ts
+++ b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -31,14 +31,14 @@ import {
opUnknownFailure,
} from "../operation.js";
import { TalerErrorCode } from "../taler-error-codes.js";
-import { codecForTalerErrorDetail } from "../wallet-types.js";
import {
- AccessToken,
- TalerBankConversionApi,
+ ConversionRate,
codecForCashinConversionResponse,
codecForCashoutConversionResponse,
codecForConversionBankConfig,
-} from "./types.js";
+} from "../types-taler-bank-conversion.js";
+import { AccessToken } from "../types-taler-common.js";
+import { codecForTalerErrorDetail } from "../types-taler-wallet.js";
import {
CacheEvictor,
makeBearerTokenAuthHeader,
@@ -193,10 +193,7 @@ export class TalerBankConversionHttpClient {
* https://docs.taler.net/core/api-bank-conversion-info.html#post--conversion-rate
*
*/
- async updateConversionRate(
- auth: AccessToken,
- body: TalerBankConversionApi.ConversionRate,
- ) {
+ async updateConversionRate(auth: AccessToken, body: ConversionRate) {
const url = new URL(`conversion-rate`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
index 6c8051ada..2639018a8 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -16,15 +16,18 @@
import {
AbsoluteTime,
+ AccessToken,
HttpStatusCode,
LibtoolVersion,
LongPollParams,
OperationAlternative,
OperationFail,
OperationOk,
+ PaginationParams,
+ TalerError,
TalerErrorCode,
- codecForChallenge,
- codecForTanTransmission,
+ UserAndToken,
+ codecForTalerCommonConfigResponse,
opKnownAlternativeFailure,
opKnownHttpFailure,
opKnownTalerFailure,
@@ -32,6 +35,7 @@ import {
import {
HttpRequestLibrary,
createPlatformHttpLib,
+ readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
@@ -42,12 +46,8 @@ import {
opSuccessFromHttp,
opUnknownFailure,
} from "../operation.js";
+import { WithdrawalOperationStatus } from "../types-taler-bank-integration.js";
import {
- AccessToken,
- PaginationParams,
- TalerCorebankApi,
- UserAndToken,
- WithdrawalOperationStatus,
codecForAccountData,
codecForBankAccountCreateWithdrawalResponse,
codecForBankAccountTransactionInfo,
@@ -55,6 +55,7 @@ import {
codecForCashoutPending,
codecForCashoutStatusResponse,
codecForCashouts,
+ codecForChallenge,
codecForCoreBankConfig,
codecForCreateTransactionResponse,
codecForGlobalCashouts,
@@ -62,8 +63,9 @@ import {
codecForMonitorResponse,
codecForPublicAccountsResponse,
codecForRegisterAccountResponse,
+ codecForTanTransmission,
codecForWithdrawalPublicInfo,
-} from "./types.js";
+} from "../types-taler-corebank.js";
import {
CacheEvictor,
IdempotencyRetry,
@@ -73,6 +75,8 @@ import {
nullEvictor,
} from "./utils.js";
+import * as TalerCorebankApi from "../types-taler-corebank.js";
+
export type TalerCoreBankResultByMethod<
prop extends keyof TalerCoreBankHttpClient,
> = ResultByMethod<TalerCoreBankHttpClient, prop>;
@@ -91,6 +95,7 @@ export enum TalerCoreBankCacheEviction {
CREATE_WITHDRAWAL,
CREATE_CASHOUT,
}
+
/**
* Protocol version spoken with the core bank.
*
@@ -123,14 +128,47 @@ export class TalerCoreBankHttpClient {
* https://docs.taler.net/core/api-corebank.html#config
*
*/
- async getConfig() {
+ async getConfig(): Promise<
+ OperationFail<HttpStatusCode.NotFound> | OperationOk<TalerCorebankApi.TalerCorebankConfigResponse>
+ > {
const url = new URL(`config`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
});
switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForCoreBankConfig());
+ case HttpStatusCode.Ok: {
+ const minBody = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForTalerCommonConfigResponse(),
+ );
+ // FIXME: Re-enable the check once fakebank and libeufin-bank return the name.
+ // const expectedName = "taler-corebank";
+ // if (minBody.name !== expectedName) {
+ // throw TalerError.fromUncheckedDetail({
+ // code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ // requestUrl: resp.requestUrl,
+ // httpStatusCode: resp.status,
+ // detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`,
+ // });
+ // }
+ if (!this.isCompatible(minBody.version)) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION,
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`,
+ });
+ }
+ // Now that we've checked the basic body, re-parse the full response.
+ const body = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForCoreBankConfig(),
+ );
+ return {
+ type: "ok",
+ body,
+ };
+ }
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index e07b6c5fa..8e98bb783 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -18,6 +18,7 @@ import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
+import { Logger } from "../logging.js";
import {
FailCasesByMethod,
ResultByMethod,
@@ -28,15 +29,15 @@ import {
opUnknownFailure,
} from "../operation.js";
import { TalerErrorCode } from "../taler-error-codes.js";
-import { codecForTalerErrorDetail } from "../wallet-types.js";
import {
- LongPollParams,
- TalerBankIntegrationApi,
+ BankWithdrawalOperationPostRequest,
WithdrawalOperationStatus,
codecForBankWithdrawalOperationPostResponse,
codecForBankWithdrawalOperationStatus,
- codecForIntegrationBankConfig,
-} from "./types.js";
+} from "../types-taler-bank-integration.js";
+import { LongPollParams } from "../types-taler-common.js";
+import { codecForIntegrationBankConfig } from "../types-taler-corebank.js";
+import { codecForTalerErrorDetail } from "../types-taler-wallet.js";
import { addLongPollingParam } from "./utils.js";
export type TalerBankIntegrationResultByMethod<
@@ -46,11 +47,15 @@ export type TalerBankIntegrationErrorsByMethod<
prop extends keyof TalerBankIntegrationHttpClient,
> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>;
+const logger = new Logger("bank-integration.ts");
+
/**
* The API is used by the wallets.
*/
export class TalerBankIntegrationHttpClient {
- public readonly PROTOCOL_VERSION = "2:0:0";
+ public static readonly PROTOCOL_VERSION = "2:0:1";
+ public readonly PROTOCOL_VERSION =
+ TalerBankIntegrationHttpClient.PROTOCOL_VERSION;
httpLib: HttpRequestLibrary;
@@ -79,6 +84,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForIntegrationBankConfig());
default:
+ logger.warn(`config request failed, status ${resp.status}`);
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -120,7 +126,7 @@ export class TalerBankIntegrationHttpClient {
*/
async completeWithdrawalOperationById(
woid: string,
- body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest,
+ body: BankWithdrawalOperationPostRequest,
) {
const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
index 8331856a9..6acff91f6 100644
--- a/packages/taler-util/src/http-client/bank-revenue.ts
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -30,12 +30,11 @@ import {
opSuccessFromHttp,
opUnknownFailure,
} from "../operation.js";
+import { LongPollParams, PaginationParams } from "../types-taler-common.js";
import {
- LongPollParams,
- PaginationParams,
codecForRevenueConfig,
codecForRevenueIncomingHistory,
-} from "./types.js";
+} from "../types-taler-revenue.js";
import { addLongPollingParam, addPaginationParams } from "./utils.js";
export type TalerBankRevenueResultByMethod<
@@ -120,7 +119,10 @@ export class TalerRevenueHttpClient {
return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
// FIXME: missing in docs
case HttpStatusCode.NoContent:
- return opFixedSuccess({incoming_transactions: [], credit_account: "" });
+ return opFixedSuccess({
+ incoming_transactions: [],
+ credit_account: "",
+ });
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Unauthorized:
diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts
index a8c976a80..84df50208 100644
--- a/packages/taler-util/src/http-client/bank-wire.ts
+++ b/packages/taler-util/src/http-client/bank-wire.ts
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpRequestLibrary, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js";
+import {
+ HttpRequestLibrary,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import {
@@ -26,16 +30,16 @@ import {
opUnknownFailure,
} from "../operation.js";
import {
- LongPollParams,
- PaginationParams,
- TalerWireGatewayApi,
codecForAddIncomingResponse,
codecForIncomingHistory,
codecForOutgoingHistory,
codecForTransferResponse,
-} from "./types.js";
+} from "../types-taler-wire-gateway.js";
import { addLongPollingParam, addPaginationParams } from "./utils.js";
+import { LongPollParams, PaginationParams } from "../types-taler-common.js";
+import * as TalerWireGatewayApi from "../types-taler-wire-gateway.js";
+
export type TalerWireGatewayResultByMethod<
prop extends keyof TalerWireGatewayHttpClient,
> = ResultByMethod<TalerWireGatewayHttpClient, prop>;
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
index aa530570d..1c6f54be2 100644
--- a/packages/taler-util/src/http-client/challenger.ts
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -1,29 +1,52 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Imports.
+ */
import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
-import { TalerCoreBankCacheEviction } from "../index.node.js";
import { LibtoolVersion } from "../libtool-version.js";
import {
FailCasesByMethod,
- RedirectResult,
ResultByMethod,
- opFixedSuccess,
opKnownAlternativeFailure,
opKnownHttpFailure,
opSuccessFromHttp,
opUnknownFailure,
} from "../operation.js";
import {
- AccessToken,
- codecForChallengeCreateResponse,
+ codecForChallengeInvalidPinResponse,
+ codecForChallengeResponse,
codecForChallengeSetupResponse,
+ codecForChallengeSolveResponse,
codecForChallengeStatus,
codecForChallengerAuthResponse,
codecForChallengerInfoResponse,
codecForChallengerTermsOfServiceResponse,
- codecForInvalidPinResponse,
-} from "./types.js";
-import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js";
+} from "../types-taler-challenger.js";
+import { AccessToken } from "../types-taler-common.js";
+import {
+ CacheEvictor,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
ResultByMethod<ChallengerHttpClient, prop>;
@@ -32,6 +55,7 @@ export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
export enum ChallengerCacheEviction {
CREATE_CHALLENGE,
+ SOLVE_CHALLENGE,
}
/**
@@ -39,13 +63,13 @@ export enum ChallengerCacheEviction {
export class ChallengerHttpClient {
httpLib: HttpRequestLibrary;
cacheEvictor: CacheEvictor<ChallengerCacheEviction>;
- public readonly PROTOCOL_VERSION = "1:0:0";
+ public readonly PROTOCOL_VERSION = "2:0:0";
constructor(
readonly baseUrl: string,
httpClient?: HttpRequestLibrary,
cacheEvictor?: CacheEvictor<ChallengerCacheEviction>,
- ) {
+ ) {
this.httpLib = httpClient ?? createPlatformHttpLib();
this.cacheEvictor = cacheEvictor ?? nullEvictor;
}
@@ -116,7 +140,6 @@ export class ChallengerHttpClient {
if (state) {
url.searchParams.set("state", state);
}
- // url.searchParams.set("scope", "code");
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
});
@@ -129,6 +152,8 @@ export class ChallengerHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.InternalServerError:
return opKnownHttpFailure(resp.status, resp);
default:
@@ -142,7 +167,7 @@ export class ChallengerHttpClient {
* https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
*
*/
- async challenge(nonce: string, body: Record<"email", string>) {
+ async challenge(nonce: string, body: Record<string, string>) {
const url = new URL(`challenge/${nonce}`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
@@ -158,13 +183,8 @@ export class ChallengerHttpClient {
await this.cacheEvictor.notifySuccess(
ChallengerCacheEviction.CREATE_CHALLENGE,
);
- return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ return opSuccessFromHttp(resp, codecForChallengeResponse());
}
- case HttpStatusCode.Found:
- const redirect = resp.headers.get("Location")!;
- return opFixedSuccess<RedirectResult>({
- redirectURL: new URL(redirect),
- });
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -197,18 +217,19 @@ export class ChallengerHttpClient {
redirect: "manual",
});
switch (resp.status) {
- case HttpStatusCode.Found:
- const redirect = resp.headers.get("Location")!;
- return opFixedSuccess<RedirectResult>({
- redirectURL: new URL(redirect),
- });
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ ChallengerCacheEviction.SOLVE_CHALLENGE,
+ );
+ return opSuccessFromHttp(resp, codecForChallengeSolveResponse());
+ }
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
return opKnownAlternativeFailure(
resp,
- resp.status,
- codecForInvalidPinResponse(),
+ HttpStatusCode.Forbidden,
+ codecForChallengeInvalidPinResponse(),
);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
index 68d68267f..cf340101e 100644
--- a/packages/taler-util/src/http-client/exchange.ts
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -1,21 +1,28 @@
-import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
import { hash } from "../nacl-fast.js";
import {
FailCasesByMethod,
+ OperationFail,
+ OperationOk,
ResultByMethod,
opEmptySuccess,
opFixedSuccess,
+ opKnownAlternativeFailure,
opKnownHttpFailure,
opSuccessFromHttp,
opUnknownFailure,
} from "../operation.js";
+import { Codec, codecForAny } from "../codec.js";
import {
TalerSignaturePurpose,
- amountToBuffer,
- bufferForUint32,
+ bufferForUint64,
buildSigPS,
decodeCrock,
eddsaSign,
@@ -24,16 +31,43 @@ import {
timestampRoundedToBuffer,
} from "../taler-crypto.js";
import {
+ AccessToken,
+ AmountString,
OfficerAccount,
PaginationParams,
+ ReserveAccount,
SigningKey,
- TalerExchangeApi,
- codecForAmlDecisionDetails,
- codecForAmlRecords,
+ codecForTalerCommonConfigResponse,
+} from "../types-taler-common.js";
+import {
+ AmlDecisionRequest,
+ BatchWithdrawResponse,
+ ExchangeBatchWithdrawRequest,
+ ExchangeVersionResponse,
+ KycRequirementInformationId,
+ WalletKycRequest,
+ codecForAccountKycStatus,
+ codecForAmlDecisionsResponse,
+ codecForAmlKycAttributes,
+ codecForAmlWalletKycCheckResponse,
+ codecForAvailableMeasureSummary,
+ codecForEventCounter,
codecForExchangeConfig,
codecForExchangeKeys,
-} from "./types.js";
-import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";
+ codecForKycProcessClientInformation,
+ codecForKycProcessStartInformation,
+ codecForLegitimizationNeededResponse,
+} from "../types-taler-exchange.js";
+import {
+ CacheEvictor,
+ addMerchantPaginationParams,
+ nullEvictor,
+} from "./utils.js";
+
+import { TalerError } from "../errors.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { codecForEmptyObject } from "../types-taler-wallet.js";
+import { canonicalJson } from "../helpers.js";
export type TalerExchangeResultByMethod<
prop extends keyof TalerExchangeHttpClient,
@@ -43,15 +77,17 @@ export type TalerExchangeErrorsByMethod<
> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
export enum TalerExchangeCacheEviction {
- CREATE_DESCISION,
+ UPLOAD_KYC_FORM,
+ MAKE_AML_DECISION,
}
-
+declare const __pubId: unique symbol;
+export type ReservePub = string & { [__pubId]: true };
/**
*/
export class TalerExchangeHttpClient {
httpLib: HttpRequestLibrary;
- public readonly PROTOCOL_VERSION = "18:0:1";
+ public readonly PROTOCOL_VERSION = "21:0:0";
cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
constructor(
@@ -67,6 +103,20 @@ export class TalerExchangeHttpClient {
const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
return compare?.compatible ?? false;
}
+
+ // TERMS
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+
+ // EXCHANGE INFORMATION
+
/**
* https://docs.taler.net/core/api-exchange.html#get--seed
*
@@ -92,20 +142,54 @@ export class TalerExchangeHttpClient {
* https://docs.taler.net/core/api-exchange.html#get--config
*
*/
- async getConfig() {
+ async getConfig(): Promise<
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationOk<ExchangeVersionResponse>
+ > {
const url = new URL(`config`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
});
switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForExchangeConfig());
+ case HttpStatusCode.Ok: {
+ const minBody = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForTalerCommonConfigResponse(),
+ );
+ const expectedName = "taler-exchange";
+ if (minBody.name !== expectedName) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`,
+ });
+ }
+ if (!this.isCompatible(minBody.version)) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION,
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`,
+ });
+ }
+ // Now that we've checked the basic body, re-parse the full response.
+ const body = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeConfig(),
+ );
+ return {
+ type: "ok",
+ body,
+ };
+ }
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
+
/**
* https://docs.taler.net/core/api-merchant.html#get--config
*
@@ -124,44 +208,705 @@ export class TalerExchangeHttpClient {
}
}
- // TERMS
+ //
+ // MANAGEMENT
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--management-keys
+ *
+ */
+ async getFutureKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-keys
+ *
+ */
+ async signFutureKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-denominations-$H_DENOM_PUB-revoke
+ *
+ */
+ async revokeFutureDenominationKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-signkeys-$EXCHANGE_PUB-revoke
+ *
+ */
+ async revokeFutureSigningKeys(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-auditors
+ *
+ */
+ async enableAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-auditors-$AUDITOR_PUB-disable
+ *
+ */
+ async disableAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire-fee
+ *
+ */
+ async configWireFee(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-global-fees
+ *
+ */
+ async configGlobalFees(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire
+ *
+ */
+ async enableWireMethod(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-wire-disable
+ *
+ */
+ async disableWireMethod(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-drain
+ *
+ */
+ async drainProfits(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-aml-officers
+ *
+ */
+ async updateOfficer(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--management-partners
+ *
+ */
+ async enablePartner(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // AUDITOR
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--auditors-$AUDITOR_PUB-$H_DENOM_PUB
+ *
+ */
+ async addAuditor(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // WITHDRAWAL
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB
+ *
+ */
+ async getReserveInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--csr-withdraw
+ *
+ */
+ async prepareCsrWithdawal(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-batch-withdraw
+ *
+ */
+ async withdraw(rid: ReservePub, body: ExchangeBatchWithdrawRequest) {
+ const url = new URL(`reserves/${rid}/batch-withdraw`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForAny() as Codec<BatchWithdrawResponse>,
+ );
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#withdraw-with-age-restriction
+ *
+ */
+ async withdrawWithAge(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--age-withdraw-$ACH-reveal
+ *
+ */
+ async revealCoinsForAge(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // RESERVE HISTORY
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB-history
+ *
+ */
+ async getResverveHistory(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // COIN HISTORY
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-history
+ *
+ */
+ async getCoinHistory(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // DEPOSIT
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--batch-deposit
+ *
+ */
+ async deposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // REFRESH
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--csr-melt
+ *
+ */
+ async prepareCsrMelt(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-melt
+ *
+ */
+ async meltCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--refreshes-$RCH-reveal
+ *
+ */
+ async releaveCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--coins-$COIN_PUB-link
+ *
+ */
+ async linkCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // RECOUP
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup
+ *
+ */
+ async recoupReserveCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup-refresh
+ *
+ */
+ async recoupRefreshCoin(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // WIRE TRANSFER
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--transfers-$WTID
+ *
+ */
+ async getWireTransferInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--deposits-$H_WIRE-$MERCHANT_PUB-$H_CONTRACT_TERMS-$COIN_PUB
+ *
+ */
+ async getWireTransferIdForDeposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // REFUND
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-refund
+ *
+ */
+ async refund(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // WALLET TO WALLET
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-merge
+ *
+ */
+ async getPurseInfoAtMerge(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--purses-$PURSE_PUB-deposit
+ *
+ */
+ async getPurseInfoAtDeposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-create
+ *
+ */
+ async createPurseFromDeposit(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#delete--purses-$PURSE_PUB
+ *
+ */
+ async deletePurse(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ *
+ */
+ async mergePurse(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse
+ *
+ */
+ async createPurseFromReserve(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-deposit
+ *
+ */
+ async depositIntoPurse(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ // WADS
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--wads-$WAD_ID
+ *
+ */
+ async getWadInfo(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ //
+ // KYC
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-wallet
+ *
+ */
+ async notifyKycBalanceLimit(account: ReserveAccount, balance: AmountString) {
+ const url = new URL(`kyc-wallet`, this.baseUrl);
+
+ const body: WalletKycRequest = {
+ balance,
+ reserve_pub: account.id,
+ reserve_sig: encodeCrock(account.signingKey),
+ };
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlWalletKycCheckResponse());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_PAYTO
+ *
+ */
+ async checkKycStatus(
+ account: ReserveAccount,
+ paytoHash: string,
+ params: {
+ timeout?: number;
+ awaitAuth?: boolean;
+ } = {},
+ ) {
+ const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl);
+
+ if (params.timeout !== undefined) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+ if (params.awaitAuth !== undefined) {
+ url.searchParams.set("await_auth", params.awaitAuth ? "YES" : "NO");
+ }
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Account-Owner-Signature": buildKYCQuerySignature(account.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForAccountKycStatus(),
+ );
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(
+ resp,
+ codecForAccountKycStatus(),
+ );
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess(undefined);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN
+ *
+ */
+ async checkKycInfo(
+ token: AccessToken,
+ known: KycRequirementInformationId[],
+ params: {
+ timeout?: number;
+ } = {},
+ ) {
+ const url = new URL(`kyc-info/${token}`, this.baseUrl);
+
+ if (params.timeout !== undefined) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "If-None-Match": known.length ? known.join(",") : undefined,
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForKycProcessClientInformation());
+ case HttpStatusCode.NoContent:
+ return opKnownAlternativeFailure(
+ resp,
+ HttpStatusCode.NoContent,
+ codecForEmptyObject(),
+ );
+ case HttpStatusCode.NotModified:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-upload-$ID
+ *
+ */
+ async uploadKycForm(requirement: KycRequirementInformationId, body: object) {
+ const url = new URL(`kyc-upload/${requirement}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerExchangeCacheEviction.UPLOAD_KYC_FORM,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PayloadTooLarge:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--kyc-start-$ID
+ *
+ */
+ async startExternalKycProcess(
+ requirement: KycRequirementInformationId,
+ body: object = {},
+ ) {
+ const url = new URL(`kyc-start/${requirement}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForKycProcessStartInformation());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PayloadTooLarge:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--kyc-proof-$PROVIDER_NAME?state=$H_PAYTO
+ *
+ */
+ async completeExternalKycProcess(provider: string, state: string, code:string) {
+ const url = new URL(`kyc-proof/${provider}?state=${state}&code=${code}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ redirect: "manual"
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.SeeOther:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+
+ }
//
// AML operations
//
/**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
+ *
+ */
+ async getAmlMesasures(auth: OfficerAccount) {
+ const url = new URL(`aml/${auth.id}/measures`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAvailableMeasureSummary());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures
*
*/
- async getDecisionsByState(
+ async getAmlKycStatistics(
auth: OfficerAccount,
- state: TalerExchangeApi.AmlState,
- pagination?: PaginationParams,
+ name: string,
+ filter: {
+ since?: Date;
+ until?: Date;
+ } = {},
) {
- const url = new URL(
- `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`,
- this.baseUrl,
- );
- addPaginationParams(url, pagination);
+ const url = new URL(`aml/${auth.id}/kyc-statistics/${name}`, this.baseUrl);
+
+ if (filter.since !== undefined) {
+ url.searchParams.set("start_date", String(filter.since.getTime()));
+ }
+ if (filter.until !== undefined) {
+ url.searchParams.set("end_date", String(filter.until.getTime()));
+ }
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
headers: {
- "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForEventCounter());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions
+ *
+ */
+ async getAmlDecisions(
+ auth: OfficerAccount,
+ params: PaginationParams & {
+ account?: string;
+ active?: boolean;
+ investigation?: boolean;
+ } = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/decisions`, this.baseUrl);
+
+ addMerchantPaginationParams(url, params);
+ if (params.account !== undefined) {
+ url.searchParams.set("h_payto", params.account);
+ }
+ if (params.active !== undefined) {
+ url.searchParams.set("active", params.active ? "YES" : "NO");
+ }
+ if (params.investigation !== undefined) {
+ url.searchParams.set(
+ "investigation",
+ params.investigation ? "YES" : "NO",
+ );
+ }
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
},
});
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAmlRecords());
+ return opSuccessFromHttp(resp, codecForAmlDecisionsResponse());
case HttpStatusCode.NoContent:
return opFixedSuccess({ records: [] });
- //this should be unauthorized
case HttpStatusCode.Forbidden:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
@@ -172,29 +917,31 @@ export class TalerExchangeHttpClient {
}
/**
- * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
*
*/
- async getDecisionDetails(auth: OfficerAccount, account: string) {
- const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl);
+ async getAmlAttributesForAccount(
+ auth: OfficerAccount,
+ account: string,
+ params: PaginationParams = {},
+ ) {
+ const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl);
+ addMerchantPaginationParams(url, params);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
headers: {
- "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
},
});
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForAmlDecisionDetails());
+ return opSuccessFromHttp(resp, codecForAmlKycAttributes());
case HttpStatusCode.NoContent:
- return opFixedSuccess({ aml_history: [], kyc_attributes: [] });
- //this should be unauthorized
+ return opFixedSuccess({ details: [] });
case HttpStatusCode.Forbidden:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
@@ -205,30 +952,33 @@ export class TalerExchangeHttpClient {
}
/**
- * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO
*
*/
- async addDecisionDetails(
+ async makeAmlDesicion(
auth: OfficerAccount,
- decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
+ decision: Omit<AmlDecisionRequest, "officer_sig">,
) {
const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);
- const body = buildDecisionSignature(auth.signingKey, decision);
+ const body = buildAMLDecisionSignature(auth.signingKey, decision);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
+ headers: {
+ "Taler-AML-Officer-Signature": buildAMLQuerySignature(auth.signingKey),
+ },
body,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerExchangeCacheEviction.MAKE_AML_DECISION,
+ );
return opEmptySuccess(resp);
- //FIXME: this should be unauthorized
+ }
case HttpStatusCode.Forbidden:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
- //FIXME: this two need to be split by error code
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
@@ -237,33 +987,84 @@ export class TalerExchangeHttpClient {
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
+
+ // RESERVE control
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-open
+ *
+ */
+ async reserveOpen(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--reserves-attest-$RESERVE_PUB
+ *
+ */
+ async getReserveAttributes(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-attest-$RESERVE_PUB
+ *
+ */
+ async signReserveAttributes(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-close
+ *
+ */
+ async closeReserve(): Promise<never> {
+ throw Error("not yet implemented");
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#delete--reserves-$RESERVE_PUB
+ *
+ */
+ async deleteReserve(): Promise<never> {
+ throw Error("not yet implemented");
+ }
}
-function buildQuerySignature(key: SigningKey): string {
- const sigBlob = buildSigPS(
- TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
- ).build();
+function buildKYCQuerySignature(key: SigningKey): string {
+ const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build();
return encodeCrock(eddsaSign(sigBlob, key));
}
-function buildDecisionSignature(
+function buildAMLQuerySignature(key: SigningKey): string {
+ const sigBlob = buildSigPS(TalerSignaturePurpose.AML_QUERY).build();
+
+ return encodeCrock(eddsaSign(sigBlob, key));
+}
+
+function buildAMLDecisionSignature(
key: SigningKey,
- decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
-): TalerExchangeApi.AmlDecision {
+ decision: Omit<AmlDecisionRequest, "officer_sig">,
+): AmlDecisionRequest {
const zero = new Uint8Array(new ArrayBuffer(64));
- const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
- //TODO: new need the null terminator, also in the exchange
- .put(hash(stringToBytes(decision.justification))) //check null
+ const sigBlob = buildSigPS(TalerSignaturePurpose.AML_DECISION)
.put(timestampRoundedToBuffer(decision.decision_time))
- .put(amountToBuffer(decision.new_threshold))
.put(decodeCrock(decision.h_payto))
- .put(zero) //kyc_requirement
- .put(bufferForUint32(decision.new_state))
+ .put(hash(stringToBytes(decision.justification)))
+ .put(hash(stringToBytes(canonicalJson(decision.properties) + "\0")))
+ .put(hash(stringToBytes(canonicalJson(decision.new_rules) + "\0")))
+ .put(
+ decision.new_measures != null
+ ? hash(stringToBytes(decision.new_measures))
+ : zero,
+ )
+ .put(bufferForUint64(decision.keep_investigating ? 1 : 0))
.build();
const officer_sig = encodeCrock(eddsaSign(sigBlob, key));
+
return {
...decision,
officer_sig,
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
index 892971fee..7f79fabac 100644
--- a/packages/taler-util/src/http-client/merchant.ts
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -19,18 +19,24 @@ import {
FailCasesByMethod,
HttpStatusCode,
LibtoolVersion,
+ OperationFail,
+ OperationOk,
PaginationParams,
ResultByMethod,
+ TalerError,
+ TalerErrorCode,
TalerMerchantApi,
+ TalerMerchantConfigResponse,
codecForAbortResponse,
codecForAccountAddResponse,
codecForAccountKycRedirects,
codecForAccountsSummaryResponse,
- codecForBankAccountEntry,
+ codecForBankAccountDetail,
+ codecForCategoryListResponse,
+ codecForCategoryProductList,
codecForClaimResponse,
codecForInstancesResponse,
codecForInventorySummaryResponse,
- codecForMerchantConfig,
codecForMerchantOrderPrivateStatusResponse,
codecForMerchantPosProductDetail,
codecForMerchantRefundResponse,
@@ -39,6 +45,7 @@ import {
codecForOtpDeviceSummaryResponse,
codecForOutOfStockResponse,
codecForPaidRefundStatusResponse,
+ codecForPaymentDeniedLegallyResponse,
codecForPaymentResponse,
codecForPostOrderResponse,
codecForProductDetail,
@@ -46,6 +53,8 @@ import {
codecForStatusGoto,
codecForStatusPaid,
codecForStatusStatusUnpaid,
+ codecForTalerCommonConfigResponse,
+ codecForTalerMerchantConfigResponse,
codecForTansferList,
codecForTemplateDetails,
codecForTemplateSummaryResponse,
@@ -63,6 +72,7 @@ import {
HttpRequestLibrary,
HttpResponse,
createPlatformHttpLib,
+ readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { opSuccessFromHttp, opUnknownFailure } from "../operation.js";
@@ -92,6 +102,9 @@ export enum TalerMerchantInstanceCacheEviction {
CREATE_PRODUCT,
UPDATE_PRODUCT,
DELETE_PRODUCT,
+ CREATE_CATEGORY,
+ UPDATE_CATEGORY,
+ DELETE_CATEGORY,
CREATE_TRANSFER,
DELETE_TRANSFER,
CREATE_DEVICE,
@@ -108,11 +121,13 @@ export enum TalerMerchantInstanceCacheEviction {
DELETE_TOKENFAMILY,
LAST,
}
+
export enum TalerMerchantManagementCacheEviction {
CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1,
UPDATE_INSTANCE,
DELETE_INSTANCE,
}
+
/**
* Protocol version spoken with the core bank.
*
@@ -123,7 +138,7 @@ export enum TalerMerchantManagementCacheEviction {
* Uses libtool's current:revision:age versioning.
*/
export class TalerMerchantInstanceHttpClient {
- public readonly PROTOCOL_VERSION = "15:0:0";
+ public readonly PROTOCOL_VERSION = "17:0:0";
readonly httpLib: HttpRequestLibrary;
readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>;
@@ -144,17 +159,48 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#get--config
- *
*/
- async getConfig() {
+ async getConfig(): Promise<
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationOk<TalerMerchantConfigResponse>
+ > {
const url = new URL(`config`, this.baseUrl);
-
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
});
switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForMerchantConfig());
+ case HttpStatusCode.Ok: {
+ const minBody = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForTalerCommonConfigResponse(),
+ );
+ const expectedName = "taler-merchant";
+ if (minBody.name !== expectedName) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`,
+ });
+ }
+ if (!this.isCompatible(minBody.version)) {
+ throw TalerError.fromUncheckedDetail({
+ code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION,
+ requestUrl: resp.requestUrl,
+ httpStatusCode: resp.status,
+ detail: `Unsupported protocol version, client supports ${this.PROTOCOL_VERSION}, server supports ${minBody.version}`,
+ });
+ }
+ // Now that we've checked the basic body, re-parse the full response.
+ const body = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForTalerMerchantConfigResponse(),
+ );
+ return {
+ type: "ok",
+ body,
+ };
+ }
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
@@ -231,6 +277,12 @@ export class TalerMerchantInstanceHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.GatewayTimeout:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPaymentDeniedLegallyResponse(),
+ );
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
@@ -386,6 +438,12 @@ export class TalerMerchantInstanceHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPaymentDeniedLegallyResponse(),
+ );
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
@@ -466,7 +524,7 @@ export class TalerMerchantInstanceHttpClient {
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private
*
*/
- async getCurrentInstanceDetails(token: AccessToken) {
+ async getCurrentInstanceDetails(token: AccessToken | undefined) {
const url = new URL(`private`, this.baseUrl);
const headers: Record<string, string> = {};
@@ -559,6 +617,8 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAccountKycRedirects());
case HttpStatusCode.Accepted:
return opSuccessFromHttp(resp, codecForAccountKycRedirects());
case HttpStatusCode.NoContent:
@@ -661,7 +721,10 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
*/
- async listBankAccounts(token: AccessToken, params?: PaginationParams) {
+ async listBankAccounts(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/accounts`, this.baseUrl);
// addMerchantPaginationParams(url, params);
@@ -707,7 +770,7 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForBankAccountEntry());
+ return opSuccessFromHttp(resp, codecForBankAccountDetail());
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -753,6 +816,173 @@ export class TalerMerchantInstanceHttpClient {
//
/**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories
+ */
+ async listCategories(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/categories`, this.baseUrl);
+
+ // addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCategoryListResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID
+ */
+ async getCategoryDetails(token: AccessToken | undefined, cId: string) {
+ const url = new URL(`private/categories/${cId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCategoryProductList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories
+ */
+ async addCategory(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.CategoryCreateRequest,
+ ) {
+ const url = new URL(`private/categories`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_CATEGORY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ // case HttpStatusCode.Conflict:
+ // return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories
+ */
+ async updateCategory(
+ token: AccessToken | undefined,
+ cid: string,
+ body: TalerMerchantApi.CategoryCreateRequest,
+ ) {
+ const url = new URL(`private/categories/${cid}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_CATEGORY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ // case HttpStatusCode.Conflict:
+ // return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID
+ */
+ async deleteCategory(token: AccessToken | undefined, cId: string) {
+ const url = new URL(`private/categories/${cId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_CATEGORY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ // case HttpStatusCode.Conflict:
+ // return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products
*/
async addProduct(
@@ -882,9 +1112,8 @@ export class TalerMerchantInstanceHttpClient {
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
-
}
-
+
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
*/
@@ -913,7 +1142,7 @@ export class TalerMerchantInstanceHttpClient {
}
/**
- * https://docs.taler.net/core/api-merchant.html#reserving-inventory
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products-$PRODUCT_ID-lock
*/
async lockProduct(
token: AccessToken | undefined,
@@ -951,7 +1180,7 @@ export class TalerMerchantInstanceHttpClient {
}
/**
- * https://docs.taler.net/core/api-merchant.html#removing-products-from-inventory
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
*/
async deleteProduct(token: AccessToken | undefined, productId: string) {
const url = new URL(`private/products/${productId}`, this.baseUrl);
@@ -1020,6 +1249,12 @@ export class TalerMerchantInstanceHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPaymentDeniedLegallyResponse(),
+ );
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Gone:
@@ -1257,6 +1492,12 @@ export class TalerMerchantInstanceHttpClient {
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForPaymentDeniedLegallyResponse(),
+ );
default:
return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
@@ -2370,3 +2611,29 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
}
}
}
+
+// 2024-09-23T01:23:14.421Z http.ts INFO malformed error response (status 200): {
+// "kyc_data": [
+// {
+// "payto_uri": "payto://iban/DE1327812254798?receiver-name=the%20name%20of%20merchant",
+// "exchange_url": "http://exchange.taler.test:1180/",
+// "no_keys": false,
+// "auth_conflict": false,
+// "exchange_http_status": 204,
+// "limits": [],
+// "payto_kycauths": [
+// "payto://iban/DE9714548806481?receiver-name=Exchanger+Normal&subject=54DR9R0CEWA1A7FK3QWABJ1PRBCD2X6S418Y5DE0P9Q1ASKTX770"
+// ]
+// },
+// {
+// "payto_uri": "payto://iban/DE1327812254798?receiver-name=the%20name%20of%20merchant",
+// "exchange_url": "https://exchange.demo.taler.net/",
+// "no_keys": false,
+// "auth_conflict": false,
+// "exchange_http_status": 400,
+// "exchange_code": 26,
+// "access_token": "0000000000000000000000000000000000000000000000000000",
+// "limits": []
+// }
+// ]
+// }
diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts
index 2c1426be2..f5f55ea1b 100644
--- a/packages/taler-util/src/http-client/officer-account.ts
+++ b/packages/taler-util/src/http-client/officer-account.ts
@@ -28,7 +28,7 @@ import {
encryptWithDerivedKey,
getRandomBytesF,
kdf,
- stringToBytes,
+ stringToBytes
} from "@gnu-taler/taler-util";
/**
@@ -50,8 +50,8 @@ export async function unlockOfficerAccount(
rawKey,
rawPassword,
password,
- ).catch((e: Error) => {
- throw new UnwrapKeyError(e.message);
+ ).catch((e) => {
+ throw new UnwrapKeyError(e instanceof Error ? e.message : String(e));
})) as SigningKey;
const publicKey = eddsaGetPublic(signingKey);
@@ -96,6 +96,41 @@ export async function createNewOfficerAccount(
return { id: accountId, signingKey, safe };
}
+/**
+ * Create new account (secured private key)
+ * secured with the given password
+ *
+ * @param extraNonce
+ * @param password
+ * @returns
+ */
+export async function createNewWalletKycAccount(
+ extraNonce: EncryptionNonce,
+ password: string,
+): Promise<OfficerAccount & { safe: LockedAccount }> {
+ const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
+
+ const key = stringToBytes(password);
+
+ const localRnd = getRandomBytesF(24);
+ const mergedRnd: EncryptionNonce = extraNonce
+ ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd)
+ : localRnd;
+
+ const protectedPrivKey = await encryptWithDerivedKey(
+ mergedRnd,
+ key,
+ eddsaPriv,
+ password,
+ );
+
+ const signingKey = eddsaPriv as SigningKey;
+ const accountId = encodeCrock(eddsaPub) as OfficerId;
+ const safe = encodeCrock(protectedPrivKey) as LockedAccount;
+
+ return { id: accountId, signingKey, safe };
+}
+
export class UnwrapKeyError extends Error {
public cause: string;
constructor(cause: string) {
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
deleted file mode 100644
index 0ef0bd65a..000000000
--- a/packages/taler-util/src/http-client/types.ts
+++ /dev/null
@@ -1,5517 +0,0 @@
-import { codecForAmountString } from "../amounts.js";
-import {
- Codec,
- buildCodecForObject,
- buildCodecForUnion,
- codecForAny,
- codecForBoolean,
- codecForConstNumber,
- codecForConstString,
- codecForEither,
- codecForList,
- codecForMap,
- codecForNumber,
- codecForString,
- codecOptional,
- codecOptionalDefault,
-} from "../codec.js";
-import { PaytoString, codecForPaytoString } from "../payto.js";
-import {
- AmountString,
- ExchangeWireAccount,
- InternationalizedString,
- codecForExchangeWireAccount,
- codecForInternationalizedString,
- codecForLocation,
-} from "../taler-types.js";
-import { TalerUriString, codecForTalerUriString } from "../taleruri.js";
-import {
- AbsoluteTime,
- TalerProtocolDuration,
- TalerProtocolTimestamp,
- codecForDuration,
- codecForTimestamp,
-} from "../time.js";
-
-export type UserAndPassword = {
- username: string;
- password: string;
-};
-
-export type UserAndToken = {
- username: string;
- token: AccessToken;
-};
-
-declare const opaque_OfficerAccount: unique symbol;
-export type LockedAccount = string & { [opaque_OfficerAccount]: true };
-
-declare const opaque_OfficerId: unique symbol;
-export type OfficerId = string & { [opaque_OfficerId]: true };
-
-declare const opaque_OfficerSigningKey: unique symbol;
-export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true };
-
-export interface OfficerAccount {
- id: OfficerId;
- signingKey: SigningKey;
-}
-
-export type PaginationParams = {
- /**
- * row identifier as the starting point of the query
- */
- offset?: string;
- /**
- * max number of element in the result response
- * always greater than 0
- */
- limit?: number;
- /**
- * order
- */
- order?: "asc" | "dec";
-};
-
-export type LongPollParams = {
- /**
- * milliseconds the server should wait for at least one result to be shown
- */
- timeoutMs?: number;
-};
-///
-/// HASH
-///
-
-// 64-byte hash code.
-type HashCode = string;
-
-type PaytoHash = string;
-
-type AmlOfficerPublicKeyP = string;
-
-// 32-byte hash code.
-type ShortHashCode = string;
-
-// 16-byte salt.
-type WireSalt = string;
-
-type SHA256HashCode = ShortHashCode;
-
-type SHA512HashCode = HashCode;
-
-// 32-byte nonce value, must only be used once.
-type CSNonce = string;
-
-// 32-byte nonce value, must only be used once.
-type RefreshMasterSeed = string;
-
-// 32-byte value representing a point on Curve25519.
-type Cs25519Point = string;
-
-// 32-byte value representing a scalar multiplier
-// for scalar operations on points on Curve25519.
-type Cs25519Scalar = string;
-
-///
-/// KEYS
-///
-
-// 16-byte access token used to authorize access.
-type ClaimToken = string;
-
-// EdDSA and ECDHE public keys always point on Curve25519
-// and represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type EddsaPublicKey = string;
-
-// EdDSA and ECDHE public keys always point on Curve25519
-// and represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type EddsaPrivateKey = string;
-
-// Edx25519 public keys are points on Curve25519 and represented using the
-// standard 256 bits Ed25519 compact format converted to Crockford
-// Base32.
-type Edx25519PublicKey = string;
-
-// Edx25519 private keys are always points on Curve25519
-// and represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type Edx25519PrivateKey = string;
-
-// EdDSA and ECDHE public keys always point on Curve25519
-// and represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type EcdhePublicKey = string;
-
-// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type CsRPublic = string;
-
-// EdDSA and ECDHE public keys always point on Curve25519
-// and represented using the standard 256 bits Ed25519 compact format,
-// converted to Crockford Base32.
-type EcdhePrivateKey = string;
-
-type CoinPublicKey = EddsaPublicKey;
-
-// RSA public key converted to Crockford Base32.
-type RsaPublicKey = string;
-
-type Integer = number;
-
-type WireTransferIdentifierRawP = string;
-// Subset of numbers: Integers in the
-// inclusive range 0 .. (2^53 - 1).
-type SafeUint64 = number;
-
-// The string must be a data URL according to RFC 2397
-// with explicit mediatype and base64 parameters.
-//
-// data:<mediatype>;base64,<data>
-//
-// Supported mediatypes are image/jpeg and image/png.
-// Invalid strings will be rejected by the wallet.
-type ImageDataUrl = string;
-
-type WadId = string;
-
-type Timestamp = TalerProtocolTimestamp;
-
-type RelativeTime = TalerProtocolDuration;
-
-export interface LoginToken {
- token: AccessToken;
- expiration: Timestamp;
-}
-
-declare const __ac_token: unique symbol;
-/**
- * Use `createAccessToken(string)` function to build one.
- */
-export type AccessToken = string & {
- [__ac_token]: true;
-};
-
-/**
- * Create a rfc8959 access token.
- * Adds secret-token: prefix if there is none.
- * Encode the token with rfc7230 to send in a http header.
- *
- * @param token
- * @returns
- */
-export function createRFC8959AccessTokenEncoded(token: string): AccessToken {
- return (
- token.startsWith("secret-token:")
- ? token
- : `secret-token:${encodeURIComponent(token)}`
- ) as AccessToken;
-}
-
-/**
- * Create a rfc8959 access token.
- * Adds secret-token: prefix if there is none.
- *
- * @param token
- * @returns
- */
-export function createRFC8959AccessTokenPlain(token: string): AccessToken {
- return (
- token.startsWith("secret-token:") ? token : `secret-token:${token}`
- ) as AccessToken;
-}
-
-/**
- * Convert string to access token.
- *
- * @param clientSecret
- * @returns
- */
-export function createClientSecretAccessToken(
- clientSecret: string,
-): AccessToken {
- return clientSecret as AccessToken;
-}
-
-declare const __officer_signature: unique symbol;
-export type OfficerSignature = string & {
- [__officer_signature]: true;
-};
-
-export namespace TalerAuthentication {
- export interface TokenRequest {
- // Service-defined scope for the token.
- // Typical scopes would be "readonly" or "readwrite".
- scope: string;
-
- // Server may impose its own upper bound
- // on the token validity duration
- duration?: RelativeTime;
-
- // Is the token refreshable into a new token during its
- // validity?
- // Refreshable tokens effectively provide indefinite
- // access if they are refreshed in time.
- refreshable?: boolean;
- }
-
- export interface TokenSuccessResponse {
- // Expiration determined by the server.
- // Can be based on the token_duration
- // from the request, but ultimately the
- // server decides the expiration.
- expiration: Timestamp;
-
- // Opque access token.
- access_token: AccessToken;
- }
- export interface TokenSuccessResponseMerchant {
- // Expiration determined by the server.
- // Can be based on the token_duration
- // from the request, but ultimately the
- // server decides the expiration.
- expiration: Timestamp;
-
- // Opque access token.
- token: AccessToken;
- }
-}
-
-// DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
-export interface CurrencySpecification {
- // Name of the currency.
- name: string;
-
- // how many digits the user may enter after the decimal_separator
- num_fractional_input_digits: Integer;
-
- // Number of fractional digits to render in normal font and size.
- num_fractional_normal_digits: Integer;
-
- // Number of fractional digits to render always, if needed by
- // padding with zeros.
- num_fractional_trailing_zero_digits: Integer;
-
- // map of powers of 10 to alternative currency names / symbols, must
- // always have an entry under "0" that defines the base name,
- // e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC".
- // Communicates the currency symbol to be used.
- alt_unit_names: { [log10: string]: string };
-}
-
-//FIXME: implement this codec
-export const codecForAccessToken = codecForString as () => Codec<AccessToken>;
-export const codecForTokenSuccessResponse =
- (): Codec<TalerAuthentication.TokenSuccessResponse> =>
- buildCodecForObject<TalerAuthentication.TokenSuccessResponse>()
- .property("access_token", codecForAccessToken())
- .property("expiration", codecForTimestamp)
- .build("TalerAuthentication.TokenSuccessResponse");
-
-export const codecForTokenSuccessResponseMerchant =
- (): Codec<TalerAuthentication.TokenSuccessResponseMerchant> =>
- buildCodecForObject<TalerAuthentication.TokenSuccessResponseMerchant>()
- .property("token", codecForAccessToken())
- .property("expiration", codecForTimestamp)
- .build("TalerAuthentication.TokenSuccessResponseMerchant");
-
-export const codecForCurrencySpecificiation =
- (): Codec<CurrencySpecification> =>
- buildCodecForObject<CurrencySpecification>()
- .property("name", codecForString())
- .property("num_fractional_input_digits", codecForNumber())
- .property("num_fractional_normal_digits", codecForNumber())
- .property("num_fractional_trailing_zero_digits", codecForNumber())
- .property("alt_unit_names", codecForMap(codecForString()))
- .build("CurrencySpecification");
-
-export const codecForIntegrationBankConfig =
- (): Codec<TalerCorebankApi.IntegrationConfig> =>
- buildCodecForObject<TalerCorebankApi.IntegrationConfig>()
- .property("name", codecForConstString("taler-bank-integration"))
- .property("version", codecForString())
- .property("currency", codecForString())
- .property("currency_specification", codecForCurrencySpecificiation())
- .build("TalerCorebankApi.IntegrationConfig");
-
-export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
- buildCodecForObject<TalerCorebankApi.Config>()
- .property("name", codecForConstString("libeufin-bank"))
- .property("version", codecForString())
- .property("bank_name", codecForString())
- .property("base_url", codecOptional(codecForString()))
- .property("allow_conversion", codecForBoolean())
- .property("allow_registrations", codecForBoolean())
- .property("allow_deletions", codecForBoolean())
- .property("allow_edit_name", codecForBoolean())
- .property("allow_edit_cashout_payto_uri", codecForBoolean())
- .property("default_debit_threshold", codecForAmountString())
- .property("currency", codecForString())
- .property("currency_specification", codecForCurrencySpecificiation())
- .property(
- "supported_tan_channels",
- codecForList(
- codecForEither(
- codecForConstString(TalerCorebankApi.TanChannel.SMS),
- codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
- ),
- ),
- )
- .property("wire_type", codecOptionalDefault(codecForString(), "iban"))
- .property("wire_transfer_fees", codecOptional(codecForAmountString()))
- .build("TalerCorebankApi.Config");
-
-//FIXME: implement this codec
-export const codecForURN = codecForString;
-
-export const codecForExchangeConfigInfo =
- (): Codec<TalerMerchantApi.ExchangeConfigInfo> =>
- buildCodecForObject<TalerMerchantApi.ExchangeConfigInfo>()
- .property("base_url", codecForString())
- .property("currency", codecForString())
- .property("master_pub", codecForString())
- .build("TalerMerchantApi.ExchangeConfigInfo");
-
-export const codecForMerchantConfig =
- (): Codec<TalerMerchantApi.VersionResponse> =>
- buildCodecForObject<TalerMerchantApi.VersionResponse>()
- .property("name", codecForConstString("taler-merchant"))
- .property("currency", codecForString())
- .property("version", codecForString())
- .property("currencies", codecForMap(codecForCurrencySpecificiation()))
- .property("exchanges", codecForList(codecForExchangeConfigInfo()))
- .build("TalerMerchantApi.VersionResponse");
-
-export const codecForClaimResponse =
- (): Codec<TalerMerchantApi.ClaimResponse> =>
- buildCodecForObject<TalerMerchantApi.ClaimResponse>()
- .property("contract_terms", codecForContractTerms())
- .property("sig", codecForString())
- .build("TalerMerchantApi.ClaimResponse");
-
-export const codecForPaymentResponse =
- (): Codec<TalerMerchantApi.PaymentResponse> =>
- buildCodecForObject<TalerMerchantApi.PaymentResponse>()
- .property("pos_confirmation", codecOptional(codecForString()))
- .property("sig", codecForString())
- .build("TalerMerchantApi.PaymentResponse");
-
-export const codecForStatusPaid = (): Codec<TalerMerchantApi.StatusPaid> =>
- buildCodecForObject<TalerMerchantApi.StatusPaid>()
- .property("refund_amount", codecForAmountString())
- .property("refund_pending", codecForBoolean())
- .property("refund_taken", codecForAmountString())
- .property("refunded", codecForBoolean())
- .property("type", codecForConstString("paid"))
- .build("TalerMerchantApi.StatusPaid");
-
-export const codecForStatusGoto =
- (): Codec<TalerMerchantApi.StatusGotoResponse> =>
- buildCodecForObject<TalerMerchantApi.StatusGotoResponse>()
- .property("public_reorder_url", codecForURL())
- .property("type", codecForConstString("goto"))
- .build("TalerMerchantApi.StatusGotoResponse");
-
-export const codecForStatusStatusUnpaid =
- (): Codec<TalerMerchantApi.StatusUnpaidResponse> =>
- buildCodecForObject<TalerMerchantApi.StatusUnpaidResponse>()
- .property("type", codecForConstString("unpaid"))
- .property("already_paid_order_id", codecOptional(codecForString()))
- .property("fulfillment_url", codecOptional(codecForString()))
- .property("taler_pay_uri", codecForTalerUriString())
- .build("TalerMerchantApi.PaymentResponse");
-
-export const codecForPaidRefundStatusResponse =
- (): Codec<TalerMerchantApi.PaidRefundStatusResponse> =>
- buildCodecForObject<TalerMerchantApi.PaidRefundStatusResponse>()
- .property("pos_confirmation", codecOptional(codecForString()))
- .property("refunded", codecForBoolean())
- .build("TalerMerchantApi.PaidRefundStatusResponse");
-
-export const codecForMerchantAbortPayRefundSuccessStatus =
- (): Codec<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus> =>
- buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("type", codecForConstString("success"))
- .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus");
-
-export const codecForMerchantAbortPayRefundFailureStatus =
- (): Codec<TalerMerchantApi.MerchantAbortPayRefundFailureStatus> =>
- buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundFailureStatus>()
- .property("exchange_code", codecForNumber())
- .property("exchange_reply", codecForAny())
- .property("exchange_status", codecForNumber())
- .property("type", codecForConstString("failure"))
- .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus");
-
-export const codecForMerchantAbortPayRefundStatus =
- (): Codec<TalerMerchantApi.MerchantAbortPayRefundStatus> =>
- buildCodecForUnion<TalerMerchantApi.MerchantAbortPayRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
- .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
- .build("TalerMerchantApi.MerchantAbortPayRefundStatus");
-
-export const codecForAbortResponse =
- (): Codec<TalerMerchantApi.AbortResponse> =>
- buildCodecForObject<TalerMerchantApi.AbortResponse>()
- .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
- .build("TalerMerchantApi.AbortResponse");
-
-export const codecForWalletRefundResponse =
- (): Codec<TalerMerchantApi.WalletRefundResponse> =>
- buildCodecForObject<TalerMerchantApi.WalletRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("refund_amount", codecForAmountString())
- .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
- .build("TalerMerchantApi.AbortResponse");
-
-export const codecForMerchantCoinRefundSuccessStatus =
- (): Codec<TalerMerchantApi.MerchantCoinRefundSuccessStatus> =>
- buildCodecForObject<TalerMerchantApi.MerchantCoinRefundSuccessStatus>()
- .property("type", codecForConstString("success"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("exchange_sig", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForAmountString())
- .property("exchange_pub", codecForString())
- .property("execution_time", codecForTimestamp)
- .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus");
-
-export const codecForMerchantCoinRefundFailureStatus =
- (): Codec<TalerMerchantApi.MerchantCoinRefundFailureStatus> =>
- buildCodecForObject<TalerMerchantApi.MerchantCoinRefundFailureStatus>()
- .property("type", codecForConstString("failure"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForNumber())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForAmountString())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("execution_time", codecForTimestamp)
- .build("TalerMerchantApi.MerchantCoinRefundFailureStatus");
-
-export const codecForMerchantCoinRefundStatus =
- (): Codec<TalerMerchantApi.MerchantCoinRefundStatus> =>
- buildCodecForUnion<TalerMerchantApi.MerchantCoinRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantCoinRefundSuccessStatus())
- .alternative("failure", codecForMerchantCoinRefundFailureStatus())
- .build("TalerMerchantApi.MerchantCoinRefundStatus");
-
-export const codecForQueryInstancesResponse =
- (): Codec<TalerMerchantApi.QueryInstancesResponse> =>
- buildCodecForObject<TalerMerchantApi.QueryInstancesResponse>()
- .property("name", codecForString())
- .property("user_type", codecForString())
- .property("email", codecOptional(codecForString()))
- .property("website", codecOptional(codecForString()))
- .property("logo", codecOptional(codecForString()))
- .property("merchant_pub", codecForString())
- .property("address", codecForLocation())
- .property("jurisdiction", codecForLocation())
- .property("use_stefan", codecForBoolean())
- .property("default_wire_transfer_delay", codecForDuration)
- .property("default_pay_delay", codecForDuration)
- .property(
- "auth",
- buildCodecForObject<{
- method: "external" | "token";
- }>()
- .property(
- "method",
- codecForEither(
- codecForConstString("token"),
- codecForConstString("external"),
- ),
- )
- .build("TalerMerchantApi.QueryInstancesResponse.auth"),
- )
- .build("TalerMerchantApi.QueryInstancesResponse");
-
-export const codecForAccountKycRedirects =
- (): Codec<TalerMerchantApi.AccountKycRedirects> =>
- buildCodecForObject<TalerMerchantApi.AccountKycRedirects>()
- .property(
- "pending_kycs",
- codecForList(codecForMerchantAccountKycRedirect()),
- )
- .property("timeout_kycs", codecForList(codecForExchangeKycTimeout()))
-
- .build("TalerMerchantApi.AccountKycRedirects");
-
-export const codecForMerchantAccountKycRedirect =
- (): Codec<TalerMerchantApi.MerchantAccountKycRedirect> =>
- buildCodecForObject<TalerMerchantApi.MerchantAccountKycRedirect>()
- .property("kyc_url", codecForURL())
- .property("aml_status", codecForNumber())
- .property("exchange_url", codecForURL())
- .property("payto_uri", codecForPaytoString())
- .build("TalerMerchantApi.MerchantAccountKycRedirect");
-
-export const codecForExchangeKycTimeout =
- (): Codec<TalerMerchantApi.ExchangeKycTimeout> =>
- buildCodecForObject<TalerMerchantApi.ExchangeKycTimeout>()
- .property("exchange_url", codecForURL())
- .property("exchange_code", codecForNumber())
- .property("exchange_http_status", codecForNumber())
- .build("TalerMerchantApi.ExchangeKycTimeout");
-
-export const codecForAccountAddResponse =
- (): Codec<TalerMerchantApi.AccountAddResponse> =>
- buildCodecForObject<TalerMerchantApi.AccountAddResponse>()
- .property("h_wire", codecForString())
- .property("salt", codecForString())
- .build("TalerMerchantApi.AccountAddResponse");
-
-export const codecForAccountsSummaryResponse =
- (): Codec<TalerMerchantApi.AccountsSummaryResponse> =>
- buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>()
- .property("accounts", codecForList(codecForBankAccountSummaryEntry()))
- .build("TalerMerchantApi.AccountsSummaryResponse");
-
-export const codecForBankAccountSummaryEntry =
- (): Codec<TalerMerchantApi.BankAccountSummaryEntry> =>
- buildCodecForObject<TalerMerchantApi.BankAccountSummaryEntry>()
- .property("payto_uri", codecForPaytoString())
- .property("h_wire", codecForString())
- .build("TalerMerchantApi.BankAccountSummaryEntry");
-
-export const codecForBankAccountEntry =
- (): Codec<TalerMerchantApi.BankAccountEntry> =>
- buildCodecForObject<TalerMerchantApi.BankAccountEntry>()
- .property("payto_uri", codecForPaytoString())
- .property("h_wire", codecForString())
- .property("salt", codecForString())
- .property("credit_facade_url", codecOptional(codecForURL()))
- .property("active", codecOptional(codecForBoolean()))
- .build("TalerMerchantApi.BankAccountEntry");
-
-export const codecForInventorySummaryResponse =
- (): Codec<TalerMerchantApi.InventorySummaryResponse> =>
- buildCodecForObject<TalerMerchantApi.InventorySummaryResponse>()
- .property("products", codecForList(codecForInventoryEntry()))
- .build("TalerMerchantApi.InventorySummaryResponse");
-
-export const codecForInventoryEntry =
- (): Codec<TalerMerchantApi.InventoryEntry> =>
- buildCodecForObject<TalerMerchantApi.InventoryEntry>()
- .property("product_id", codecForString())
- .property("product_serial", codecForNumber())
- .build("TalerMerchantApi.InventoryEntry");
-
-export const codecForMerchantPosProductDetail =
- (): Codec<TalerMerchantApi.MerchantPosProductDetail> =>
- buildCodecForObject<TalerMerchantApi.MerchantPosProductDetail>()
- .property("product_serial", codecForNumber())
- .property("product_id", codecOptional(codecForString()))
- .property("categories", codecForList(codecForNumber()))
- .property("description", codecForString())
- .property("description_i18n", codecForInternationalizedString())
- .property("unit", codecForString())
- .property("price", codecForAmountString())
- .property("image", codecForString())
- .property("taxes", codecOptional(codecForList(codecForTax())))
- .property("total_stock", codecForNumber())
- .property("minimum_age", codecOptional(codecForNumber()))
- .build("TalerMerchantApi.MerchantPosProductDetail");
-
-export const codecForMerchantCategory =
- (): Codec<TalerMerchantApi.MerchantCategory> =>
- buildCodecForObject<TalerMerchantApi.MerchantCategory>()
- .property("id", codecForNumber())
- .property("name", codecForString())
- .property("name_i18n", codecForInternationalizedString())
- .build("TalerMerchantApi.MerchantCategory");
-
-export const codecForFullInventoryDetailsResponse =
- (): Codec<TalerMerchantApi.FullInventoryDetailsResponse> =>
- buildCodecForObject<TalerMerchantApi.FullInventoryDetailsResponse>()
- .property("categories", codecForList(codecForMerchantCategory()))
- .property("products", codecForList(codecForMerchantPosProductDetail()))
- .build("TalerMerchantApi.FullInventoryDetailsResponse");
-
-export const codecForProductDetail =
- (): Codec<TalerMerchantApi.ProductDetail> =>
- buildCodecForObject<TalerMerchantApi.ProductDetail>()
- .property("description", codecForString())
- .property("description_i18n", codecForInternationalizedString())
- .property("unit", codecForString())
- .property("price", codecForAmountString())
- .property("image", codecForString())
- .property("taxes", codecOptional(codecForList(codecForTax())))
- .property("address", codecOptional(codecForLocation()))
- .property("next_restock", codecOptional(codecForTimestamp))
- .property("total_stock", codecForNumber())
- .property("total_sold", codecForNumber())
- .property("total_lost", codecForNumber())
- .property("minimum_age", codecOptional(codecForNumber()))
- .build("TalerMerchantApi.ProductDetail");
-
-export const codecForTax = (): Codec<TalerMerchantApi.Tax> =>
- buildCodecForObject<TalerMerchantApi.Tax>()
- .property("name", codecForString())
- .property("tax", codecForAmountString())
- .build("TalerMerchantApi.Tax");
-
-export const codecForPostOrderResponse =
- (): Codec<TalerMerchantApi.PostOrderResponse> =>
- buildCodecForObject<TalerMerchantApi.PostOrderResponse>()
- .property("order_id", codecForString())
- .property("token", codecOptional(codecForString()))
- .build("TalerMerchantApi.PostOrderResponse");
-
-export const codecForOutOfStockResponse =
- (): Codec<TalerMerchantApi.OutOfStockResponse> =>
- buildCodecForObject<TalerMerchantApi.OutOfStockResponse>()
- .property("product_id", codecForString())
- .property("available_quantity", codecForNumber())
- .property("requested_quantity", codecForNumber())
- .property("restock_expected", codecForTimestamp)
- .build("TalerMerchantApi.OutOfStockResponse");
-
-export const codecForOrderHistory = (): Codec<TalerMerchantApi.OrderHistory> =>
- buildCodecForObject<TalerMerchantApi.OrderHistory>()
- .property("orders", codecForList(codecForOrderHistoryEntry()))
- .build("TalerMerchantApi.OrderHistory");
-
-export const codecForOrderHistoryEntry =
- (): Codec<TalerMerchantApi.OrderHistoryEntry> =>
- buildCodecForObject<TalerMerchantApi.OrderHistoryEntry>()
- .property("order_id", codecForString())
- .property("row_id", codecForNumber())
- .property("timestamp", codecForTimestamp)
- .property("amount", codecForAmountString())
- .property("summary", codecForString())
- .property("refundable", codecForBoolean())
- .property("paid", codecForBoolean())
- .build("TalerMerchantApi.OrderHistoryEntry");
-
-export const codecForMerchant = (): Codec<TalerMerchantApi.Merchant> =>
- buildCodecForObject<TalerMerchantApi.Merchant>()
- .property("name", codecForString())
- .property("email", codecOptional(codecForString()))
- .property("logo", codecOptional(codecForString()))
- .property("website", codecOptional(codecForString()))
- .property("address", codecOptional(codecForLocation()))
- .property("jurisdiction", codecOptional(codecForLocation()))
- .build("TalerMerchantApi.MerchantInfo");
-
-export const codecForExchange = (): Codec<TalerMerchantApi.Exchange> =>
- buildCodecForObject<TalerMerchantApi.Exchange>()
- .property("master_pub", codecForString())
- .property("priority", codecForNumber())
- .property("url", codecForString())
- .build("TalerMerchantApi.Exchange");
-
-export const codecForContractTerms =
- (): Codec<TalerMerchantApi.ContractTerms> =>
- buildCodecForObject<TalerMerchantApi.ContractTerms>()
- .property("order_id", codecForString())
- .property("fulfillment_url", codecOptional(codecForString()))
- .property("fulfillment_message", codecOptional(codecForString()))
- .property(
- "fulfillment_message_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("merchant_base_url", codecForString())
- .property("h_wire", codecForString())
- .property("auto_refund", codecOptional(codecForDuration))
- .property("wire_method", codecForString())
- .property("summary", codecForString())
- .property(
- "summary_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("nonce", codecForString())
- .property("amount", codecForAmountString())
- .property("pay_deadline", codecForTimestamp)
- .property("refund_deadline", codecForTimestamp)
- .property("wire_transfer_deadline", codecForTimestamp)
- .property("timestamp", codecForTimestamp)
- .property("delivery_location", codecOptional(codecForLocation()))
- .property("delivery_date", codecOptional(codecForTimestamp))
- .property("max_fee", codecForAmountString())
- .property("merchant", codecForMerchant())
- .property("merchant_pub", codecForString())
- .property("exchanges", codecForList(codecForExchange()))
- .property("products", codecForList(codecForProduct()))
- .property("extra", codecForAny())
- .build("TalerMerchantApi.ContractTerms");
-
-export const codecForProduct = (): Codec<TalerMerchantApi.Product> =>
- buildCodecForObject<TalerMerchantApi.Product>()
- .property("product_id", codecOptional(codecForString()))
- .property("description", codecForString())
- .property(
- "description_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("quantity", codecOptional(codecForNumber()))
- .property("unit", codecOptional(codecForString()))
- .property("price", codecOptional(codecForAmountString()))
- .property("image", codecOptional(codecForString()))
- .property("taxes", codecOptional(codecForList(codecForTax())))
- .property("delivery_date", codecOptional(codecForTimestamp))
- .build("TalerMerchantApi.Product");
-
-export const codecForCheckPaymentPaidResponse =
- (): Codec<TalerMerchantApi.CheckPaymentPaidResponse> =>
- buildCodecForObject<TalerMerchantApi.CheckPaymentPaidResponse>()
- .property("order_status", codecForConstString("paid"))
- .property("refunded", codecForBoolean())
- .property("refund_pending", codecForBoolean())
- .property("wired", codecForBoolean())
- .property("deposit_total", codecForAmountString())
- .property("exchange_code", codecForNumber())
- .property("exchange_http_status", codecForNumber())
- .property("refund_amount", codecForAmountString())
- .property("contract_terms", codecForContractTerms())
- .property("wire_reports", codecForList(codecForTransactionWireReport()))
- .property("wire_details", codecForList(codecForTransactionWireTransfer()))
- .property("refund_details", codecForList(codecForRefundDetails()))
- .property("order_status_url", codecForURL())
- .build("TalerMerchantApi.CheckPaymentPaidResponse");
-
-export const codecForCheckPaymentUnpaidResponse =
- (): Codec<TalerMerchantApi.CheckPaymentUnpaidResponse> =>
- buildCodecForObject<TalerMerchantApi.CheckPaymentUnpaidResponse>()
- .property("order_status", codecForConstString("unpaid"))
- .property("taler_pay_uri", codecForTalerUriString())
- .property("creation_time", codecForTimestamp)
- .property("summary", codecForString())
- .property("total_amount", codecForAmountString())
- .property("already_paid_order_id", codecOptional(codecForString()))
- .property("already_paid_fulfillment_url", codecOptional(codecForString()))
- .property("order_status_url", codecForString())
- .build("TalerMerchantApi.CheckPaymentPaidResponse");
-
-export const codecForCheckPaymentClaimedResponse =
- (): Codec<TalerMerchantApi.CheckPaymentClaimedResponse> =>
- buildCodecForObject<TalerMerchantApi.CheckPaymentClaimedResponse>()
- .property("order_status", codecForConstString("claimed"))
- .property("contract_terms", codecForContractTerms())
- .build("TalerMerchantApi.CheckPaymentClaimedResponse");
-
-export const codecForMerchantOrderPrivateStatusResponse =
- (): Codec<TalerMerchantApi.MerchantOrderStatusResponse> =>
- buildCodecForUnion<TalerMerchantApi.MerchantOrderStatusResponse>()
- .discriminateOn("order_status")
- .alternative("paid", codecForCheckPaymentPaidResponse())
- .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
- .alternative("claimed", codecForCheckPaymentClaimedResponse())
- .build("TalerMerchantApi.MerchantOrderStatusResponse");
-
-export const codecForRefundDetails =
- (): Codec<TalerMerchantApi.RefundDetails> =>
- buildCodecForObject<TalerMerchantApi.RefundDetails>()
- .property("reason", codecForString())
- .property("pending", codecForBoolean())
- .property("timestamp", codecForTimestamp)
- .property("amount", codecForAmountString())
- .build("TalerMerchantApi.RefundDetails");
-
-export const codecForTransactionWireTransfer =
- (): Codec<TalerMerchantApi.TransactionWireTransfer> =>
- buildCodecForObject<TalerMerchantApi.TransactionWireTransfer>()
- .property("exchange_url", codecForURL())
- .property("wtid", codecForString())
- .property("execution_time", codecForTimestamp)
- .property("amount", codecForAmountString())
- .property("confirmed", codecForBoolean())
- .build("TalerMerchantApi.TransactionWireTransfer");
-
-export const codecForTransactionWireReport =
- (): Codec<TalerMerchantApi.TransactionWireReport> =>
- buildCodecForObject<TalerMerchantApi.TransactionWireReport>()
- .property("code", codecForNumber())
- .property("hint", codecForString())
- .property("exchange_code", codecForNumber())
- .property("exchange_http_status", codecForNumber())
- .property("coin_pub", codecForString())
- .build("TalerMerchantApi.TransactionWireReport");
-
-export const codecForMerchantRefundResponse =
- (): Codec<TalerMerchantApi.MerchantRefundResponse> =>
- buildCodecForObject<TalerMerchantApi.MerchantRefundResponse>()
- .property("taler_refund_uri", codecForTalerUriString())
- .property("h_contract", codecForString())
- .build("TalerMerchantApi.MerchantRefundResponse");
-
-export const codecForTansferList = (): Codec<TalerMerchantApi.TransferList> =>
- buildCodecForObject<TalerMerchantApi.TransferList>()
- .property("transfers", codecForList(codecForTransferDetails()))
- .build("TalerMerchantApi.TransferList");
-
-export const codecForTransferDetails =
- (): Codec<TalerMerchantApi.TransferDetails> =>
- buildCodecForObject<TalerMerchantApi.TransferDetails>()
- .property("credit_amount", codecForAmountString())
- .property("wtid", codecForString())
- .property("payto_uri", codecForPaytoString())
- .property("exchange_url", codecForURL())
- .property("transfer_serial_id", codecForNumber())
- .property("execution_time", codecOptional(codecForTimestamp))
- .property("verified", codecOptional(codecForBoolean()))
- .property("confirmed", codecOptional(codecForBoolean()))
- .build("TalerMerchantApi.TransferDetails");
-
-export const codecForOtpDeviceSummaryResponse =
- (): Codec<TalerMerchantApi.OtpDeviceSummaryResponse> =>
- buildCodecForObject<TalerMerchantApi.OtpDeviceSummaryResponse>()
- .property("otp_devices", codecForList(codecForOtpDeviceEntry()))
- .build("TalerMerchantApi.OtpDeviceSummaryResponse");
-
-export const codecForOtpDeviceEntry =
- (): Codec<TalerMerchantApi.OtpDeviceEntry> =>
- buildCodecForObject<TalerMerchantApi.OtpDeviceEntry>()
- .property("otp_device_id", codecForString())
- .property("device_description", codecForString())
- .build("TalerMerchantApi.OtpDeviceEntry");
-
-export const codecForOtpDeviceDetails =
- (): Codec<TalerMerchantApi.OtpDeviceDetails> =>
- buildCodecForObject<TalerMerchantApi.OtpDeviceDetails>()
- .property("device_description", codecForString())
- .property("otp_algorithm", codecForNumber())
- .property("otp_ctr", codecOptional(codecForNumber()))
- .property("otp_timestamp", codecForNumber())
- .property("otp_code", codecOptional(codecForString()))
- .build("TalerMerchantApi.OtpDeviceDetails");
-
-export const codecForTemplateSummaryResponse =
- (): Codec<TalerMerchantApi.TemplateSummaryResponse> =>
- buildCodecForObject<TalerMerchantApi.TemplateSummaryResponse>()
- .property("templates", codecForList(codecForTemplateEntry()))
- .build("TalerMerchantApi.TemplateSummaryResponse");
-
-export const codecForTemplateEntry =
- (): Codec<TalerMerchantApi.TemplateEntry> =>
- buildCodecForObject<TalerMerchantApi.TemplateEntry>()
- .property("template_id", codecForString())
- .property("template_description", codecForString())
- .build("TalerMerchantApi.TemplateEntry");
-
-export const codecForTemplateDetails =
- (): Codec<TalerMerchantApi.TemplateDetails> =>
- buildCodecForObject<TalerMerchantApi.TemplateDetails>()
- .property("template_description", codecForString())
- .property("otp_id", codecOptional(codecForString()))
- .property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
- .property(
- "editable_defaults",
- codecOptional(codecForTemplateContractDetailsDefaults()),
- )
- .build("TalerMerchantApi.TemplateDetails");
-
-export const codecForTemplateContractDetails =
- (): Codec<TalerMerchantApi.TemplateContractDetails> =>
- buildCodecForObject<TalerMerchantApi.TemplateContractDetails>()
- .property("summary", codecOptional(codecForString()))
- .property("currency", codecOptional(codecForString()))
- .property("amount", codecOptional(codecForAmountString()))
- .property("minimum_age", codecForNumber())
- .property("pay_duration", codecForDuration)
- .build("TalerMerchantApi.TemplateContractDetails");
-
-export const codecForTemplateContractDetailsDefaults =
- (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> =>
- buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>()
- .property("summary", codecOptional(codecForString()))
- .property("currency", codecOptional(codecForString()))
- .property("amount", codecOptional(codecForAmountString()))
- .build("TalerMerchantApi.TemplateContractDetailsDefaults");
-
-export const codecForWalletTemplateDetails =
- (): Codec<TalerMerchantApi.WalletTemplateDetails> =>
- buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
- .property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
- .property(
- "editable_defaults",
- codecOptional(codecForTemplateContractDetailsDefaults()),
- )
- .build("TalerMerchantApi.WalletTemplateDetails");
-
-export const codecForWebhookSummaryResponse =
- (): Codec<TalerMerchantApi.WebhookSummaryResponse> =>
- buildCodecForObject<TalerMerchantApi.WebhookSummaryResponse>()
- .property("webhooks", codecForList(codecForWebhookEntry()))
- .build("TalerMerchantApi.WebhookSummaryResponse");
-
-export const codecForWebhookEntry = (): Codec<TalerMerchantApi.WebhookEntry> =>
- buildCodecForObject<TalerMerchantApi.WebhookEntry>()
- .property("webhook_id", codecForString())
- .property("event_type", codecForString())
- .build("TalerMerchantApi.WebhookEntry");
-
-export const codecForWebhookDetails =
- (): Codec<TalerMerchantApi.WebhookDetails> =>
- buildCodecForObject<TalerMerchantApi.WebhookDetails>()
- .property("event_type", codecForString())
- .property("url", codecForString())
- .property("http_method", codecForString())
- .property("header_template", codecOptional(codecForString()))
- .property("body_template", codecOptional(codecForString()))
- .build("TalerMerchantApi.WebhookDetails");
-
-export const codecForTokenFamilyKind =
- (): Codec<TalerMerchantApi.TokenFamilyKind> =>
- codecForEither(
- codecForConstString("discount"),
- codecForConstString("subscription"),
- ) as any; //FIXME: create a codecForEnum
-export const codecForTokenFamilyDetails =
- (): Codec<TalerMerchantApi.TokenFamilyDetails> =>
- buildCodecForObject<TalerMerchantApi.TokenFamilyDetails>()
- .property("slug", codecForString())
- .property("name", codecForString())
- .property("description", codecForString())
- .property("description_i18n", codecForInternationalizedString())
- .property("valid_after", codecForTimestamp)
- .property("valid_before", codecForTimestamp)
- .property("duration", codecForDuration)
- .property("kind", codecForTokenFamilyKind())
- .property("issued", codecForNumber())
- .property("redeemed", codecForNumber())
- .build("TalerMerchantApi.TokenFamilyDetails");
-
-export const codecForTokenFamiliesList =
- (): Codec<TalerMerchantApi.TokenFamiliesList> =>
- buildCodecForObject<TalerMerchantApi.TokenFamiliesList>()
- .property("token_families", codecForList(codecForTokenFamilySummary()))
- .build("TalerMerchantApi.TokenFamiliesList");
-
-export const codecForTokenFamilySummary =
- (): Codec<TalerMerchantApi.TokenFamilySummary> =>
- buildCodecForObject<TalerMerchantApi.TokenFamilySummary>()
- .property("slug", codecForString())
- .property("name", codecForString())
- .property("valid_after", codecForTimestamp)
- .property("valid_before", codecForTimestamp)
- .property("kind", codecForTokenFamilyKind())
- .build("TalerMerchantApi.TokenFamilySummary");
-
-export const codecForInstancesResponse =
- (): Codec<TalerMerchantApi.InstancesResponse> =>
- buildCodecForObject<TalerMerchantApi.InstancesResponse>()
- .property("instances", codecForList(codecForInstance()))
- .build("TalerMerchantApi.InstancesResponse");
-
-export const codecForInstance = (): Codec<TalerMerchantApi.Instance> =>
- buildCodecForObject<TalerMerchantApi.Instance>()
- .property("name", codecForString())
- .property("user_type", codecForString())
- .property("website", codecOptional(codecForString()))
- .property("logo", codecOptional(codecForString()))
- .property("id", codecForString())
- .property("merchant_pub", codecForString())
- .property("payment_targets", codecForList(codecForString()))
- .property("deleted", codecForBoolean())
- .build("TalerMerchantApi.Instance");
-
-export const codecForExchangeConfig =
- (): Codec<TalerExchangeApi.ExchangeVersionResponse> =>
- buildCodecForObject<TalerExchangeApi.ExchangeVersionResponse>()
- .property("version", codecForString())
- .property("name", codecForConstString("taler-exchange"))
- .property("implementation", codecOptional(codecForURN()))
- .property("currency", codecForString())
- .property("currency_specification", codecForCurrencySpecificiation())
- .property("supported_kyc_requirements", codecForList(codecForString()))
- .build("TalerExchangeApi.ExchangeVersionResponse");
-
-export const codecForExchangeKeys =
- (): Codec<TalerExchangeApi.ExchangeKeysResponse> =>
- buildCodecForObject<TalerExchangeApi.ExchangeKeysResponse>()
- .property("version", codecForString())
- .property("base_url", codecForString())
- .property("currency", codecForString())
- .build("TalerExchangeApi.ExchangeKeysResponse");
-
-const codecForBalance = (): Codec<TalerCorebankApi.Balance> =>
- buildCodecForObject<TalerCorebankApi.Balance>()
- .property("amount", codecForAmountString())
- .property(
- "credit_debit_indicator",
- codecForEither(
- codecForConstString("credit"),
- codecForConstString("debit"),
- ),
- )
- .build("TalerCorebankApi.Balance");
-
-const codecForPublicAccount = (): Codec<TalerCorebankApi.PublicAccount> =>
- buildCodecForObject<TalerCorebankApi.PublicAccount>()
- .property("username", codecForString())
- .property("balance", codecForBalance())
- .property("payto_uri", codecForPaytoString())
- .property("is_taler_exchange", codecForBoolean())
- .property("row_id", codecOptional(codecForNumber()))
- .build("TalerCorebankApi.PublicAccount");
-
-export const codecForPublicAccountsResponse =
- (): Codec<TalerCorebankApi.PublicAccountsResponse> =>
- buildCodecForObject<TalerCorebankApi.PublicAccountsResponse>()
- .property("public_accounts", codecForList(codecForPublicAccount()))
- .build("TalerCorebankApi.PublicAccountsResponse");
-
-export const codecForAccountMinimalData =
- (): Codec<TalerCorebankApi.AccountMinimalData> =>
- buildCodecForObject<TalerCorebankApi.AccountMinimalData>()
- .property("username", codecForString())
- .property("name", codecForString())
- .property("payto_uri", codecForPaytoString())
- .property("balance", codecForBalance())
- .property("row_id", codecForNumber())
- .property("debit_threshold", codecForAmountString())
- .property("min_cashout", codecOptional(codecForAmountString()))
- .property("is_public", codecForBoolean())
- .property("is_taler_exchange", codecForBoolean())
- .property(
- "status",
- codecOptional(
- codecForEither(
- codecForConstString("active"),
- codecForConstString("deleted"),
- ),
- ),
- )
- .build("TalerCorebankApi.AccountMinimalData");
-
-export const codecForListBankAccountsResponse =
- (): Codec<TalerCorebankApi.ListBankAccountsResponse> =>
- buildCodecForObject<TalerCorebankApi.ListBankAccountsResponse>()
- .property("accounts", codecForList(codecForAccountMinimalData()))
- .build("TalerCorebankApi.ListBankAccountsResponse");
-
-export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
- buildCodecForObject<TalerCorebankApi.AccountData>()
- .property("name", codecForString())
- .property("balance", codecForBalance())
- .property("payto_uri", codecForPaytoString())
- .property("debit_threshold", codecForAmountString())
- .property("min_cashout", codecOptional(codecForAmountString()))
- .property("contact_data", codecOptional(codecForChallengeContactData()))
- .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
- .property("is_public", codecForBoolean())
- .property("is_taler_exchange", codecForBoolean())
- .property(
- "tan_channel",
- codecOptional(
- codecForEither(
- codecForConstString(TalerCorebankApi.TanChannel.SMS),
- codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
- ),
- ),
- )
- .property(
- "status",
- codecOptional(
- codecForEither(
- codecForConstString("active"),
- codecForConstString("deleted"),
- ),
- ),
- )
- .build("TalerCorebankApi.AccountData");
-
-export const codecForChallengeContactData =
- (): Codec<TalerCorebankApi.ChallengeContactData> =>
- buildCodecForObject<TalerCorebankApi.ChallengeContactData>()
- .property("email", codecOptional(codecForString()))
- .property("phone", codecOptional(codecForString()))
- .build("TalerCorebankApi.ChallengeContactData");
-
-export const codecForWithdrawalPublicInfo =
- (): Codec<TalerCorebankApi.WithdrawalPublicInfo> =>
- buildCodecForObject<TalerCorebankApi.WithdrawalPublicInfo>()
- .property(
- "status",
- codecForEither(
- codecForConstString("pending"),
- codecForConstString("selected"),
- codecForConstString("aborted"),
- codecForConstString("confirmed"),
- ),
- )
- .property("amount", codecForAmountString())
- .property("username", codecForString())
- .property("selected_reserve_pub", codecOptional(codecForString()))
- .property(
- "selected_exchange_account",
- codecOptional(codecForPaytoString()),
- )
- .build("TalerCorebankApi.WithdrawalPublicInfo");
-
-export const codecForBankAccountTransactionsResponse =
- (): Codec<TalerCorebankApi.BankAccountTransactionsResponse> =>
- buildCodecForObject<TalerCorebankApi.BankAccountTransactionsResponse>()
- .property(
- "transactions",
- codecForList(codecForBankAccountTransactionInfo()),
- )
- .build("TalerCorebankApi.BankAccountTransactionsResponse");
-
-export const codecForBankAccountTransactionInfo =
- (): Codec<TalerCorebankApi.BankAccountTransactionInfo> =>
- buildCodecForObject<TalerCorebankApi.BankAccountTransactionInfo>()
- .property("creditor_payto_uri", codecForPaytoString())
- .property("debtor_payto_uri", codecForPaytoString())
- .property("amount", codecForAmountString())
- .property(
- "direction",
- codecForEither(
- codecForConstString("debit"),
- codecForConstString("credit"),
- ),
- )
- .property("subject", codecForString())
- .property("row_id", codecForNumber())
- .property("date", codecForTimestamp)
- .build("TalerCorebankApi.BankAccountTransactionInfo");
-
-export const codecForCreateTransactionResponse =
- (): Codec<TalerCorebankApi.CreateTransactionResponse> =>
- buildCodecForObject<TalerCorebankApi.CreateTransactionResponse>()
- .property("row_id", codecForNumber())
- .build("TalerCorebankApi.CreateTransactionResponse");
-
-export const codecForRegisterAccountResponse =
- (): Codec<TalerCorebankApi.RegisterAccountResponse> =>
- buildCodecForObject<TalerCorebankApi.RegisterAccountResponse>()
- .property("internal_payto_uri", codecForPaytoString())
- .build("TalerCorebankApi.RegisterAccountResponse");
-
-export const codecForBankAccountCreateWithdrawalResponse =
- (): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> =>
- buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>()
- .property("taler_withdraw_uri", codecForTalerUriString())
- .property("withdrawal_id", codecForString())
- .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
-
-export const codecForCashoutPending =
- (): Codec<TalerCorebankApi.CashoutResponse> =>
- buildCodecForObject<TalerCorebankApi.CashoutResponse>()
- .property("cashout_id", codecForNumber())
- .build("TalerCorebankApi.CashoutPending");
-
-export const codecForCashoutConversionResponse =
- (): Codec<TalerBankConversionApi.CashoutConversionResponse> =>
- buildCodecForObject<TalerBankConversionApi.CashoutConversionResponse>()
- .property("amount_credit", codecForAmountString())
- .property("amount_debit", codecForAmountString())
- .build("TalerCorebankApi.CashoutConversionResponse");
-
-export const codecForCashinConversionResponse =
- (): Codec<TalerBankConversionApi.CashinConversionResponse> =>
- buildCodecForObject<TalerBankConversionApi.CashinConversionResponse>()
- .property("amount_credit", codecForAmountString())
- .property("amount_debit", codecForAmountString())
- .build("TalerCorebankApi.CashinConversionResponse");
-
-export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> =>
- buildCodecForObject<TalerCorebankApi.Cashouts>()
- .property("cashouts", codecForList(codecForCashoutInfo()))
- .build("TalerCorebankApi.Cashouts");
-
-export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> =>
- buildCodecForObject<TalerCorebankApi.CashoutInfo>()
- .property("cashout_id", codecForNumber())
- .build("TalerCorebankApi.CashoutInfo");
-
-export const codecForGlobalCashouts =
- (): Codec<TalerCorebankApi.GlobalCashouts> =>
- buildCodecForObject<TalerCorebankApi.GlobalCashouts>()
- .property("cashouts", codecForList(codecForGlobalCashoutInfo()))
- .build("TalerCorebankApi.GlobalCashouts");
-
-export const codecForGlobalCashoutInfo =
- (): Codec<TalerCorebankApi.GlobalCashoutInfo> =>
- buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>()
- .property("cashout_id", codecForNumber())
- .property("username", codecForString())
- .build("TalerCorebankApi.GlobalCashoutInfo");
-
-export const codecForCashoutStatusResponse =
- (): Codec<TalerCorebankApi.CashoutStatusResponse> =>
- buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>()
- .property("amount_debit", codecForAmountString())
- .property("amount_credit", codecForAmountString())
- .property("subject", codecForString())
- .property("creation_time", codecForTimestamp)
- .build("TalerCorebankApi.CashoutStatusResponse");
-
-export const codecForConversionRatesResponse =
- (): Codec<TalerCorebankApi.ConversionRatesResponse> =>
- buildCodecForObject<TalerCorebankApi.ConversionRatesResponse>()
- .property("buy_at_ratio", codecForDecimalNumber())
- .property("buy_in_fee", codecForDecimalNumber())
- .property("sell_at_ratio", codecForDecimalNumber())
- .property("sell_out_fee", codecForDecimalNumber())
- .build("TalerCorebankApi.ConversionRatesResponse");
-
-export const codecForMonitorResponse =
- (): Codec<TalerCorebankApi.MonitorResponse> =>
- buildCodecForUnion<TalerCorebankApi.MonitorResponse>()
- .discriminateOn("type")
- .alternative("no-conversions", codecForMonitorNoConversion())
- .alternative("with-conversions", codecForMonitorWithCashout())
- .build("TalerWireGatewayApi.IncomingBankTransaction");
-
-export const codecForMonitorNoConversion =
- (): Codec<TalerCorebankApi.MonitorNoConversion> =>
- buildCodecForObject<TalerCorebankApi.MonitorNoConversion>()
- .property("type", codecForConstString("no-conversions"))
- .property("talerInCount", codecForNumber())
- .property("talerInVolume", codecForAmountString())
- .property("talerOutCount", codecForNumber())
- .property("talerOutVolume", codecForAmountString())
- .build("TalerCorebankApi.MonitorJustPayouts");
-
-export const codecForMonitorWithCashout =
- (): Codec<TalerCorebankApi.MonitorWithConversion> =>
- buildCodecForObject<TalerCorebankApi.MonitorWithConversion>()
- .property("type", codecForConstString("with-conversions"))
- .property("cashinCount", codecForNumber())
- .property("cashinFiatVolume", codecForAmountString())
- .property("cashinRegionalVolume", codecForAmountString())
- .property("cashoutCount", codecForNumber())
- .property("cashoutFiatVolume", codecForAmountString())
- .property("cashoutRegionalVolume", codecForAmountString())
- .property("talerInCount", codecForNumber())
- .property("talerInVolume", codecForAmountString())
- .property("talerOutCount", codecForNumber())
- .property("talerOutVolume", codecForAmountString())
- .build("TalerCorebankApi.MonitorWithCashout");
-
-export const codecForBankVersion =
- (): Codec<TalerBankIntegrationApi.BankVersion> =>
- buildCodecForObject<TalerBankIntegrationApi.BankVersion>()
- .property("currency", codecForCurrencyName())
- .property("currency_specification", codecForCurrencySpecificiation())
- .property("name", codecForConstString("taler-bank-integration"))
- .property("version", codecForLibtoolVersion())
- .build("TalerBankIntegrationApi.BankVersion");
-
-export const codecForBankWithdrawalOperationStatus =
- (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationStatus> =>
- buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationStatus>()
- .property(
- "status",
- codecForEither(
- codecForConstString("pending"),
- codecForConstString("selected"),
- codecForConstString("aborted"),
- codecForConstString("confirmed"),
- ),
- )
- .property("amount", codecOptional(codecForAmountString()))
- .property("currency", codecOptional(codecForCurrencyName()))
- .property("suggested_amount", codecOptional(codecForAmountString()))
- .property("card_fees", codecOptional(codecForAmountString()))
- .property("sender_wire", codecOptional(codecForPaytoString()))
- .property("suggested_exchange", codecOptional(codecForURL()))
- .property("confirm_transfer_url", codecOptional(codecForURL()))
- .property("wire_types", codecForList(codecForString()))
- .property("selected_reserve_pub", codecOptional(codecForString()))
- .property("selected_exchange_account", codecOptional(codecForString()))
- .build("TalerBankIntegrationApi.BankWithdrawalOperationStatus");
-
-export const codecForBankWithdrawalOperationPostResponse =
- (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse>()
- .property(
- "status",
- codecForEither(
- codecForConstString("selected"),
- codecForConstString("aborted"),
- codecForConstString("confirmed"),
- ),
- )
- .property("confirm_transfer_url", codecOptional(codecForURL()))
- .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse");
-
-export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> =>
- buildCodecForObject<TalerRevenueApi.RevenueConfig>()
- .property("name", codecForConstString("taler-revenue"))
- .property("version", codecForString())
- .property("currency", codecForString())
- .property("implementation", codecOptional(codecForString()))
- .build("TalerRevenueApi.RevenueConfig");
-
-export const codecForRevenueIncomingHistory =
- (): Codec<TalerRevenueApi.RevenueIncomingHistory> =>
- buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>()
- .property("credit_account", codecForPaytoString())
- .property(
- "incoming_transactions",
- codecForList(codecForRevenueIncomingBankTransaction()),
- )
- .build("TalerRevenueApi.MerchantIncomingHistory");
-
-export const codecForRevenueIncomingBankTransaction =
- (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> =>
- buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>()
- .property("amount", codecForAmountString())
- .property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoString())
- .property("row_id", codecForNumber())
- .property("subject", codecForString())
- .build("TalerRevenueApi.RevenueIncomingBankTransaction");
-
-export const codecForTransferResponse =
- (): Codec<TalerWireGatewayApi.TransferResponse> =>
- buildCodecForObject<TalerWireGatewayApi.TransferResponse>()
- .property("row_id", codecForNumber())
- .property("timestamp", codecForTimestamp)
- .build("TalerWireGatewayApi.TransferResponse");
-
-export const codecForIncomingHistory =
- (): Codec<TalerWireGatewayApi.IncomingHistory> =>
- buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
- .property("credit_account", codecForPaytoString())
- .property(
- "incoming_transactions",
- codecForList(codecForIncomingBankTransaction()),
- )
- .build("TalerWireGatewayApi.IncomingHistory");
-
-export const codecForIncomingBankTransaction =
- (): Codec<TalerWireGatewayApi.IncomingBankTransaction> =>
- buildCodecForUnion<TalerWireGatewayApi.IncomingBankTransaction>()
- .discriminateOn("type")
- .alternative("RESERVE", codecForIncomingReserveTransaction())
- .alternative("WAD", codecForIncomingWadTransaction())
- .build("TalerWireGatewayApi.IncomingBankTransaction");
-
-export const codecForIncomingReserveTransaction =
- (): Codec<TalerWireGatewayApi.IncomingReserveTransaction> =>
- buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
- .property("amount", codecForAmountString())
- .property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoString())
- .property("reserve_pub", codecForString())
- .property("row_id", codecForNumber())
- .property("type", codecForConstString("RESERVE"))
- .build("TalerWireGatewayApi.IncomingReserveTransaction");
-
-export const codecForIncomingWadTransaction =
- (): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
- buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
- .property("amount", codecForAmountString())
- .property("credit_account", codecForPaytoString())
- .property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoString())
- .property("origin_exchange_url", codecForURL())
- .property("row_id", codecForNumber())
- .property("type", codecForConstString("WAD"))
- .property("wad_id", codecForString())
- .build("TalerWireGatewayApi.IncomingWadTransaction");
-
-export const codecForOutgoingHistory =
- (): Codec<TalerWireGatewayApi.OutgoingHistory> =>
- buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
- .property("debit_account", codecForPaytoString())
- .property(
- "outgoing_transactions",
- codecForList(codecForOutgoingBankTransaction()),
- )
- .build("TalerWireGatewayApi.OutgoingHistory");
-
-export const codecForOutgoingBankTransaction =
- (): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
- buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
- .property("amount", codecForAmountString())
- .property("credit_account", codecForPaytoString())
- .property("date", codecForTimestamp)
- .property("exchange_base_url", codecForURL())
- .property("row_id", codecForNumber())
- .property("wtid", codecForString())
- .build("TalerWireGatewayApi.OutgoingBankTransaction");
-
-export const codecForAddIncomingResponse =
- (): Codec<TalerWireGatewayApi.AddIncomingResponse> =>
- buildCodecForObject<TalerWireGatewayApi.AddIncomingResponse>()
- .property("row_id", codecForNumber())
- .property("timestamp", codecForTimestamp)
- .build("TalerWireGatewayApi.AddIncomingResponse");
-
-export const codecForAmlRecords = (): Codec<TalerExchangeApi.AmlRecords> =>
- buildCodecForObject<TalerExchangeApi.AmlRecords>()
- .property("records", codecForList(codecForAmlRecord()))
- .build("TalerExchangeApi.AmlRecords");
-
-export const codecForAmlRecord = (): Codec<TalerExchangeApi.AmlRecord> =>
- buildCodecForObject<TalerExchangeApi.AmlRecord>()
- .property("current_state", codecForNumber())
- .property("h_payto", codecForString())
- .property("rowid", codecForNumber())
- .property("threshold", codecForAmountString())
- .build("TalerExchangeApi.AmlRecord");
-
-export const codecForAmlDecisionDetails =
- (): Codec<TalerExchangeApi.AmlDecisionDetails> =>
- buildCodecForObject<TalerExchangeApi.AmlDecisionDetails>()
- .property("aml_history", codecForList(codecForAmlDecisionDetail()))
- .property("kyc_attributes", codecForList(codecForKycDetail()))
- .build("TalerExchangeApi.AmlDecisionDetails");
-
-export const codecForAmlDecisionDetail =
- (): Codec<TalerExchangeApi.AmlDecisionDetail> =>
- buildCodecForObject<TalerExchangeApi.AmlDecisionDetail>()
- .property("justification", codecForString())
- .property("new_state", codecForNumber())
- .property("decision_time", codecForTimestamp)
- .property("new_threshold", codecForAmountString())
- .property("decider_pub", codecForString())
- .build("TalerExchangeApi.AmlDecisionDetail");
-
-export const codecForChallenge = (): Codec<TalerCorebankApi.Challenge> =>
- buildCodecForObject<TalerCorebankApi.Challenge>()
- .property("challenge_id", codecForNumber())
- .build("TalerCorebankApi.Challenge");
-
-export const codecForTanTransmission =
- (): Codec<TalerCorebankApi.TanTransmission> =>
- buildCodecForObject<TalerCorebankApi.TanTransmission>()
- .property(
- "tan_channel",
- codecForEither(
- codecForConstString(TalerCorebankApi.TanChannel.SMS),
- codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
- ),
- )
- .property("tan_info", codecForString())
- .build("TalerCorebankApi.TanTransmission");
-
-interface KycDetail {
- provider_section: string;
- attributes?: Object;
- collection_time: Timestamp;
- expiration_time: Timestamp;
-}
-export const codecForKycDetail = (): Codec<TalerExchangeApi.KycDetail> =>
- buildCodecForObject<TalerExchangeApi.KycDetail>()
- .property("provider_section", codecForString())
- .property("attributes", codecOptional(codecForAny()))
- .property("collection_time", codecForTimestamp)
- .property("expiration_time", codecForTimestamp)
- .build("TalerExchangeApi.KycDetail");
-
-export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> =>
- buildCodecForObject<TalerExchangeApi.AmlDecision>()
- .property("justification", codecForString())
- .property("new_threshold", codecForAmountString())
- .property("h_payto", codecForString())
- .property("new_state", codecForNumber())
- .property("officer_sig", codecForString())
- .property("decision_time", codecForTimestamp)
- .property("kyc_requirements", codecOptional(codecForList(codecForString())))
- .build("TalerExchangeApi.AmlDecision");
-
-export const codecForConversionInfo =
- (): Codec<TalerBankConversionApi.ConversionInfo> =>
- buildCodecForObject<TalerBankConversionApi.ConversionInfo>()
- .property("cashin_fee", codecForAmountString())
- .property("cashin_min_amount", codecForAmountString())
- .property("cashin_ratio", codecForDecimalNumber())
- .property(
- "cashin_rounding_mode",
- codecForEither(
- codecForConstString("zero"),
- codecForConstString("up"),
- codecForConstString("nearest"),
- ),
- )
- .property("cashin_tiny_amount", codecForAmountString())
- .property("cashout_fee", codecForAmountString())
- .property("cashout_min_amount", codecForAmountString())
- .property("cashout_ratio", codecForDecimalNumber())
- .property(
- "cashout_rounding_mode",
- codecForEither(
- codecForConstString("zero"),
- codecForConstString("up"),
- codecForConstString("nearest"),
- ),
- )
- .property("cashout_tiny_amount", codecForAmountString())
- .build("ConversionBankConfig.ConversionInfo");
-
-export const codecForConversionBankConfig =
- (): Codec<TalerBankConversionApi.IntegrationConfig> =>
- buildCodecForObject<TalerBankConversionApi.IntegrationConfig>()
- .property("name", codecForConstString("taler-conversion-info"))
- .property("version", codecForString())
- .property("regional_currency", codecForString())
- .property(
- "regional_currency_specification",
- codecForCurrencySpecificiation(),
- )
- .property("fiat_currency", codecForString())
- .property("fiat_currency_specification", codecForCurrencySpecificiation())
-
- .property("conversion_rate", codecForConversionInfo())
- .build("ConversionBankConfig.IntegrationConfig");
-
-export const codecForChallengerTermsOfServiceResponse =
- (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> =>
- buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>()
- .property("name", codecForConstString("challenger"))
- .property("version", codecForString())
- .property("implementation", codecOptional(codecForString()))
- .build("ChallengerApi.ChallengerTermsOfServiceResponse");
-
-export const codecForChallengeSetupResponse =
- (): Codec<ChallengerApi.ChallengeSetupResponse> =>
- buildCodecForObject<ChallengerApi.ChallengeSetupResponse>()
- .property("nonce", codecForString())
- .build("ChallengerApi.ChallengeSetupResponse");
-
-export const codecForChallengeStatus =
- (): Codec<ChallengerApi.ChallengeStatus> =>
- buildCodecForObject<ChallengerApi.ChallengeStatus>()
- .property("restrictions", codecOptional(codecForMap(codecForAny())))
- .property("fix_address", codecForBoolean())
- .property("last_address", codecOptional(codecForMap(codecForAny())))
- .property("changes_left", codecForNumber())
- .build("ChallengerApi.ChallengeStatus");
-export const codecForChallengeCreateResponse =
- (): Codec<ChallengerApi.ChallengeCreateResponse> =>
- buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
- .property("attempts_left", codecForNumber())
- .property("address", codecForAny())
- .property("transmitted", codecForBoolean())
- .property("next_tx_time", codecForString())
- .build("ChallengerApi.ChallengeCreateResponse");
-
-export const codecForInvalidPinResponse =
- (): Codec<ChallengerApi.InvalidPinResponse> =>
- buildCodecForObject<ChallengerApi.InvalidPinResponse>()
- .property("ec", codecOptional(codecForNumber()))
- .property("hint", codecForAny())
- .property("addresses_left", codecForNumber())
- .property("pin_transmissions_left", codecForNumber())
- .property("auth_attempts_left", codecForNumber())
- .property("exhausted", codecForBoolean())
- .property("no_challenge", codecForBoolean())
- .build("ChallengerApi.InvalidPinResponse");
-
-export const codecForChallengerAuthResponse =
- (): Codec<ChallengerApi.ChallengerAuthResponse> =>
- buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
- .property("access_token", codecForString())
- .property("token_type", codecForAny())
- .property("expires_in", codecForNumber())
- .build("ChallengerApi.ChallengerAuthResponse");
-
-export const codecForChallengerInfoResponse =
- (): Codec<ChallengerApi.ChallengerInfoResponse> =>
- buildCodecForObject<ChallengerApi.ChallengerInfoResponse>()
- .property("id", codecForNumber())
- .property("address", codecForAny())
- .property("address_type", codecForString())
- .property("expires", codecForTimestamp)
- .build("ChallengerApi.ChallengerInfoResponse");
-
-export const codecForTemplateEditableDetails =
- (): Codec<TalerMerchantApi.TemplateEditableDetails> =>
- buildCodecForObject<TalerMerchantApi.TemplateEditableDetails>()
- .property("summary", codecOptional(codecForString()))
- .property("currency", codecOptional(codecForString()))
- .property("amount", codecOptional(codecForAmountString()))
- .build("TemplateEditableDetails");
-
-export const codecForMerchantReserveCreateConfirmation =
- (): Codec<TalerMerchantApi.MerchantReserveCreateConfirmation> =>
- buildCodecForObject<TalerMerchantApi.MerchantReserveCreateConfirmation>()
- .property("accounts", codecForList(codecForExchangeWireAccount()))
- .property("reserve_pub", codecForString())
- .build("MerchantReserveCreateConfirmation");
-
-type EmailAddress = string;
-type PhoneNumber = string;
-type EddsaSignature = string;
-// base32 encoded RSA blinded signature.
-type BlindedRsaSignature = string;
-type Base32 = string;
-
-type DecimalNumber = string;
-type RsaSignature = string;
-type Float = number;
-type LibtoolVersion = string;
-// The type of a coin's blinded envelope depends on the cipher that is used
-// for signing with a denomination key.
-type CoinEnvelope = RSACoinEnvelope | CSCoinEnvelope;
-// For denomination signatures based on RSA, the planchet is just a blinded
-// coin's public EdDSA key.
-interface RSACoinEnvelope {
- cipher: "RSA" | "RSA+age_restricted";
- rsa_blinded_planchet: string; // Crockford Base32 encoded
-}
-// For denomination signatures based on Blind Clause-Schnorr, the planchet
-// consists of the public nonce and two Curve25519 scalars which are two
-// blinded challenges in the Blinded Clause-Schnorr signature scheme.
-// See https://taler.net/papers/cs-thesis.pdf for details.
-interface CSCoinEnvelope {
- cipher: "CS" | "CS+age_restricted";
- cs_nonce: string; // Crockford Base32 encoded
- cs_blinded_c0: string; // Crockford Base32 encoded
- cs_blinded_c1: string; // Crockford Base32 encoded
-}
-// Secret for blinding/unblinding.
-// An RSA blinding secret, which is basically
-// a 256-bit nonce, converted to Crockford Base32.
-type DenominationBlindingKeyP = string;
-
-//FIXME: implement this codec
-const codecForURL = codecForString;
-//FIXME: implement this codec
-const codecForLibtoolVersion = codecForString;
-//FIXME: implement this codec
-const codecForCurrencyName = codecForString;
-//FIXME: implement this codec
-const codecForDecimalNumber = codecForString;
-
-export type WithdrawalOperationStatus =
- | "pending"
- | "selected"
- | "aborted"
- | "confirmed";
-
-export namespace TalerWireGatewayApi {
- export interface TransferResponse {
- // Timestamp that indicates when the wire transfer will be executed.
- // In cases where the wire transfer gateway is unable to know when
- // the wire transfer will be executed, the time at which the request
- // has been received and stored will be returned.
- // The purpose of this field is for debugging (humans trying to find
- // the transaction) as well as for taxation (determining which
- // time period a transaction belongs to).
- timestamp: Timestamp;
-
- // Opaque ID of the transaction that the bank has made.
- row_id: SafeUint64;
- }
-
- export interface TransferRequest {
- // Nonce to make the request idempotent. Requests with the same
- // transaction_uid that differ in any of the other fields
- // are rejected.
- request_uid: HashCode;
-
- // Amount to transfer.
- amount: AmountString;
-
- // Base URL of the exchange. Shall be included by the bank gateway
- // in the appropriate section of the wire transfer details.
- exchange_base_url: string;
-
- // Wire transfer identifier chosen by the exchange,
- // used by the merchant to identify the Taler order(s)
- // associated with this wire transfer.
- wtid: ShortHashCode;
-
- // The recipient's account identifier as a payto URI.
- credit_account: PaytoString;
- }
-
- export interface IncomingHistory {
- // Array of incoming transactions.
- incoming_transactions: IncomingBankTransaction[];
-
- // Payto URI to identify the receiver of funds.
- // This must be one of the exchange's bank accounts.
- // Credit account is shared by all incoming transactions
- // as per the nature of the request.
-
- // undefined if incoming transaction is empty
- credit_account?: PaytoString;
- }
-
- // Union discriminated by the "type" field.
- export type IncomingBankTransaction =
- | IncomingReserveTransaction
- | IncomingWadTransaction;
-
- export interface IncomingReserveTransaction {
- type: "RESERVE";
-
- // Opaque identifier of the returned record.
- row_id: SafeUint64;
-
- // Date of the transaction.
- date: Timestamp;
-
- // Amount transferred.
- amount: AmountString;
-
- // Payto URI to identify the sender of funds.
- debit_account: PaytoString;
-
- // The reserve public key extracted from the transaction details.
- reserve_pub: EddsaPublicKey;
- }
-
- export interface IncomingWadTransaction {
- type: "WAD";
-
- // Opaque identifier of the returned record.
- row_id: SafeUint64;
-
- // Date of the transaction.
- date: Timestamp;
-
- // Amount transferred.
- amount: AmountString;
-
- // Payto URI to identify the receiver of funds.
- // This must be one of the exchange's bank accounts.
- credit_account: PaytoString;
-
- // Payto URI to identify the sender of funds.
- debit_account: PaytoString;
-
- // Base URL of the exchange that originated the wad.
- origin_exchange_url: string;
-
- // The reserve public key extracted from the transaction details.
- wad_id: WadId;
- }
-
- export interface OutgoingHistory {
- // Array of outgoing transactions.
- outgoing_transactions: OutgoingBankTransaction[];
-
- // Payto URI to identify the sender of funds.
- // This must be one of the exchange's bank accounts.
- // Credit account is shared by all incoming transactions
- // as per the nature of the request.
-
- // undefined if outgoing transactions is empty
- debit_account?: PaytoString;
- }
-
- export interface OutgoingBankTransaction {
- // Opaque identifier of the returned record.
- row_id: SafeUint64;
-
- // Date of the transaction.
- date: Timestamp;
-
- // Amount transferred.
- amount: AmountString;
-
- // Payto URI to identify the receiver of funds.
- credit_account: PaytoString;
-
- // The wire transfer ID in the outgoing transaction.
- wtid: ShortHashCode;
-
- // Base URL of the exchange.
- exchange_base_url: string;
- }
-
- export interface AddIncomingRequest {
- // Amount to transfer.
- amount: AmountString;
-
- // Reserve public key that is included in the wire transfer details
- // to identify the reserve that is being topped up.
- reserve_pub: EddsaPublicKey;
-
- // Account (as payto URI) that makes the wire transfer to the exchange.
- // Usually this account must be created by the test harness before this API is
- // used. An exception is the "exchange-fakebank", where any debit account can be
- // specified, as it is automatically created.
- debit_account: PaytoString;
- }
-
- export interface AddIncomingResponse {
- // Timestamp that indicates when the wire transfer will be executed.
- // In cases where the wire transfer gateway is unable to know when
- // the wire transfer will be executed, the time at which the request
- // has been received and stored will be returned.
- // The purpose of this field is for debugging (humans trying to find
- // the transaction) as well as for taxation (determining which
- // time period a transaction belongs to).
- timestamp: Timestamp;
-
- // Opaque ID of the transaction that the bank has made.
- row_id: SafeUint64;
- }
-}
-
-export namespace TalerRevenueApi {
- export interface RevenueConfig {
- // Name of the API.
- name: "taler-revenue";
-
- // libtool-style representation of the Bank protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- version: string;
-
- // Currency used by this gateway.
- currency: string;
-
- // URN of the implementation (needed to interpret 'revision' in version).
- // @since v0, may become mandatory in the future.
- implementation?: string;
- }
-
- export interface RevenueIncomingHistory {
- // Array of incoming transactions.
- incoming_transactions: RevenueIncomingBankTransaction[];
-
- // Payto URI to identify the receiver of funds.
- // Credit account is shared by all incoming transactions
- // as per the nature of the request.
- credit_account: string;
- }
-
- export interface RevenueIncomingBankTransaction {
- // Opaque identifier of the returned record.
- row_id: SafeUint64;
-
- // Date of the transaction.
- date: Timestamp;
-
- // Amount transferred.
- amount: AmountString;
-
- // Payto URI to identify the sender of funds.
- debit_account: string;
-
- // The wire transfer subject.
- subject: string;
- }
-}
-
-export namespace TalerBankConversionApi {
- export interface ConversionInfo {
- // Exchange rate to buy regional currency from fiat
- cashin_ratio: DecimalNumber;
-
- // Exchange rate to sell regional currency for fiat
- cashout_ratio: DecimalNumber;
-
- // Fee to subtract after applying the cashin ratio.
- cashin_fee: AmountString;
-
- // Fee to subtract after applying the cashout ratio.
- cashout_fee: AmountString;
-
- // Minimum amount authorised for cashin, in fiat before conversion
- cashin_min_amount: AmountString;
-
- // Minimum amount authorised for cashout, in regional before conversion
- cashout_min_amount: AmountString;
-
- // Smallest possible regional amount, converted amount is rounded to this amount
- cashin_tiny_amount: AmountString;
-
- // Smallest possible fiat amount, converted amount is rounded to this amount
- cashout_tiny_amount: AmountString;
-
- // Rounding mode used during cashin conversion
- cashin_rounding_mode: "zero" | "up" | "nearest";
-
- // Rounding mode used during cashout conversion
- cashout_rounding_mode: "zero" | "up" | "nearest";
- }
-
- export interface IntegrationConfig {
- // libtool-style representation of the Bank 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 API.
- name: "taler-conversion-info";
-
- // Currency used by this bank.
- regional_currency: string;
-
- // How the bank SPA should render this currency.
- regional_currency_specification: CurrencySpecification;
-
- // External currency used during conversion.
- fiat_currency: string;
-
- // How the bank SPA should render this currency.
- fiat_currency_specification: CurrencySpecification;
-
- // Extra conversion rate information.
- // Only present if server opts in to report the static conversion rate.
- conversion_rate: ConversionInfo;
- }
-
- export interface CashinConversionResponse {
- // Amount that the user will get deducted from their fiat
- // bank account, according to the 'amount_credit' value.
- amount_debit: AmountString;
- // Amount that the user will receive in their regional
- // bank account, according to 'amount_debit'.
- amount_credit: AmountString;
- }
-
- export interface CashoutConversionResponse {
- // Amount that the user will get deducted from their regional
- // bank account, according to the 'amount_credit' value.
- amount_debit: AmountString;
- // Amount that the user will receive in their fiat
- // bank account, according to 'amount_debit'.
- amount_credit: AmountString;
- }
-
- export type RoundingMode = "zero" | "up" | "nearest";
-
- export interface ConversionRate {
- // Exchange rate to buy regional currency from fiat
- cashin_ratio: DecimalNumber;
-
- // Fee to subtract after applying the cashin ratio.
- cashin_fee: AmountString;
-
- // Minimum amount authorised for cashin, in fiat before conversion
- cashin_min_amount: AmountString;
-
- // Smallest possible regional amount, converted amount is rounded to this amount
- cashin_tiny_amount: AmountString;
-
- // Rounding mode used during cashin conversion
- cashin_rounding_mode: RoundingMode;
-
- // Exchange rate to sell regional currency for fiat
- cashout_ratio: DecimalNumber;
-
- // Fee to subtract after applying the cashout ratio.
- cashout_fee: AmountString;
-
- // Minimum amount authorised for cashout, in regional before conversion
- cashout_min_amount: AmountString;
-
- // Smallest possible fiat amount, converted amount is rounded to this amount
- cashout_tiny_amount: AmountString;
-
- // Rounding mode used during cashout conversion
- cashout_rounding_mode: RoundingMode;
- }
-}
-
-export namespace TalerBankIntegrationApi {
- export interface BankVersion {
- // libtool-style representation of the Bank protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- version: string;
-
- // Currency used by this bank.
- currency: string;
-
- // How the bank SPA should render this currency.
- currency_specification?: CurrencySpecification;
-
- // Name of the API.
- name: "taler-bank-integration";
- }
-
- export interface BankWithdrawalOperationStatus {
- // Current status of the operation
- // pending: the operation is pending parameters selection (exchange and reserve public key)
- // selected: the operations has been selected and is pending confirmation
- // aborted: the operation has been aborted
- // confirmed: the transfer has been confirmed and registered by the bank
- status: WithdrawalOperationStatus;
-
- // Currency used for the withdrawal.
- // MUST be present when amount is absent.
- // @since v2, may become mandatory in the future.
- currency?: string;
-
- // Amount that will be withdrawn with this operation
- // (raw amount without fee considerations). Only
- // given once the amount is fixed and cannot be changed.
- // Optional since **vC2EC**.
- amount?: AmountString | undefined;
-
- // Suggestion for the amount to be withdrawn with this
- // operation. Given if a suggestion was made but the
- // user may still change the amount.
- // Optional since **vC2EC**.
- suggested_amount?: AmountString | undefined;
-
- // Maximum amount that the wallet can choose to withdraw.
- // Only applicable when the amount is not fixed.
- // @since **vC2EC**.
- max_amount?: AmountString | undefined;
-
- // The non-Taler card fees the customer will have
- // to pay to the bank / payment service provider
- // they are using to make the withdrawal.
- // @since **vC2EC**
- card_fees?: AmountString | undefined;
-
- // Bank account of the customer that is debiting, as an
- // RFC 8905 payto URI.
- sender_wire?: PaytoString;
-
- // Base URL of the suggested exchange. The bank may have
- // neither a suggestion nor a requirement for the exchange.
- // This value is typically set in the bank's configuration.
- suggested_exchange?: string;
-
- // Base URL of an exchange that must be used. Optional,
- // not given *unless* a particular exchange is mandatory.
- // This value is typically set in the bank's configuration.
- // @since **vC2EC**
- required_exchange?: string;
-
- // URL that the user needs to navigate to in order to
- // complete some final confirmation (e.g. 2FA).
- // Only applicable when status is selected or pending.
- // It may contain the withdrawal operation id.
- confirm_transfer_url?: string;
-
- // Wire transfer types supported by the bank.
- wire_types: string[];
-
- // Reserve public key selected by the exchange,
- // only non-null if status is selected or confirmed.
- selected_reserve_pub?: string;
-
- // Exchange account selected by the wallet;
- // only non-null if status is selected or confirmed.
- // @since **v1**
- selected_exchange_account?: string;
- }
-
- export interface BankWithdrawalOperationPostRequest {
- // Reserve public key that should become the wire transfer
- // subject to fund the withdrawal.
- reserve_pub: string;
-
- // Payto address of the exchange selected for the withdrawal.
- selected_exchange: PaytoString;
-
- // Selected amount to be transferred. Optional if the
- // backend already knows the amount.
- // @since **vC2EC**
- amount?: AmountString | undefined;
- }
-
- export interface BankWithdrawalOperationPostResponse {
- // Current status of the operation
- // pending: the operation is pending parameters selection (exchange and reserve public key)
- // selected: the operations has been selected and is pending confirmation
- // aborted: the operation has been aborted
- // confirmed: the transfer has been confirmed and registered by the bank
- status: Omit<"pending", WithdrawalOperationStatus>;
-
- // URL that the user needs to navigate to in order to
- // complete some final confirmation (e.g. 2FA).
- //
- // Only applicable when status is selected or pending.
- // It may contain withdrawal operation id
- confirm_transfer_url?: string;
- }
-}
-
-export namespace TalerCorebankApi {
- export interface IntegrationConfig {
- // libtool-style representation of the Bank protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- version: string;
-
- currency: string;
-
- // How the bank SPA should render this currency.
- currency_specification: CurrencySpecification;
-
- // Name of the API.
- name: "taler-bank-integration";
- }
- export interface Config {
- // Name of this API, always "taler-corebank".
- name: "libeufin-bank";
- // name: "taler-corebank";
-
- // API version in the form $n:$n:$n
- version: string;
-
- // Bank display name to be used in user interfaces.
- // For consistency use "Taler Bank" if missing.
- // @since v4, will become mandatory in the next version.
- bank_name: string;
-
- // Advertised base URL to use when you sharing an URL with another
- // program.
- // @since v4.
- base_url?: string;
-
- // If 'true' the server provides local currency conversion support
- // If 'false' some parts of the API are not supported and return 501
- allow_conversion: boolean;
-
- // If 'true' anyone can register
- // If 'false' only the admin can
- allow_registrations: boolean;
-
- // If 'true' account can delete themselves
- // If 'false' only the admin can delete accounts
- allow_deletions: boolean;
-
- // If 'true' anyone can edit their name
- // If 'false' only admin can
- allow_edit_name: boolean;
-
- // If 'true' anyone can edit their cashout account
- // If 'false' only the admin
- allow_edit_cashout_payto_uri: boolean;
-
- // Default debt limit for newly created accounts
- default_debit_threshold: AmountString;
-
- // Currency used by this bank.
- currency: string;
-
- // How the bank SPA should render this currency.
- currency_specification: CurrencySpecification;
-
- // TAN channels supported by the server
- supported_tan_channels: TanChannel[];
-
- // Wire transfer type supported by the bank.
- // Default to 'iban' is missing
- // @since v4, may become mandatory in the future.
- wire_type: string;
-
- // Wire transfer execution fees.
- // @since v4, will become mandatory in the next version.
- wire_transfer_fees?: AmountString;
- }
-
- export interface BankAccountCreateWithdrawalRequest {
- // Amount to withdraw. If given, the wallet
- // cannot change the amount.
- // Optional since **vC2EC**.
- amount?: AmountString;
-
- // Suggested amount to withdraw. The wallet can
- // still change the suggestion.
- // @since **vC2EC**
- suggested_amount?: AmountString;
-
- // The non-Taler card fees the customer will have
- // to pay to the account owner, bank and/or
- // payment service provider
- // they are using to make this withdrawal.
- // @since **vC2EC**
- card_fees?: AmountString;
- }
-
- export interface BankAccountCreateWithdrawalResponse {
- // ID of the withdrawal, can be used to view/modify the withdrawal operation.
- withdrawal_id: string;
-
- // URI that can be passed to the wallet to initiate the withdrawal.
- taler_withdraw_uri: TalerUriString;
- }
- export interface WithdrawalPublicInfo {
- // Current status of the operation
- // pending: the operation is pending parameters selection (exchange and reserve public key)
- // selected: the operations has been selected and is pending confirmation
- // aborted: the operation has been aborted
- // confirmed: the transfer has been confirmed and registered by the bank
- status: WithdrawalOperationStatus;
-
- // Amount that will be withdrawn with this operation
- // (raw amount without fee considerations).
- amount: AmountString;
-
- // Account username
- username: string;
-
- // Reserve public key selected by the exchange,
- // only non-null if status is selected or confirmed.
- selected_reserve_pub?: string;
-
- // Exchange account selected by the wallet
- // only non-null if status is selected or confirmed.
- selected_exchange_account?: PaytoString;
- }
-
- export interface BankAccountTransactionsResponse {
- transactions: BankAccountTransactionInfo[];
- }
-
- export interface BankAccountTransactionInfo {
- creditor_payto_uri: PaytoString;
- debtor_payto_uri: PaytoString;
-
- amount: AmountString;
- direction: "debit" | "credit";
-
- subject: string;
-
- // Transaction unique ID. Matches
- // $transaction_id from the URI.
- row_id: number;
- date: Timestamp;
- }
-
- export interface CreateTransactionRequest {
- // Address in the Payto format of the wire transfer receiver.
- // It needs at least the 'message' query string parameter.
- payto_uri: PaytoString;
-
- // Transaction amount (in the $currency:x.y format), optional.
- // However, when not given, its value must occupy the 'amount'
- // query string parameter of the 'payto' field. In case it
- // is given in both places, the paytoUri's takes the precedence.
- amount?: AmountString;
-
- // Nonce to make the request idempotent. Requests with the same
- // request_uid that differ in any of the other fields
- // are rejected.
- // @since v4, will become mandatory in the next version.
- request_uid?: ShortHashCode;
- }
-
- export interface CreateTransactionResponse {
- // ID identifying the transaction being created
- row_id: Integer;
- }
-
- export interface RegisterAccountResponse {
- // Internal payto URI of this bank account.
- internal_payto_uri: PaytoString;
- }
-
- export interface RegisterAccountRequest {
- // Username
- username: string;
-
- // Password.
- password: string;
-
- // Legal name of the account owner
- name: string;
-
- // Defaults to false.
- is_public?: boolean;
-
- // Is this a taler exchange account?
- // If true:
- // - incoming transactions to the account that do not
- // have a valid reserve public key are automatically
- // - the account provides the taler-wire-gateway-api endpoints
- // Defaults to false.
- is_taler_exchange?: boolean;
-
- // Addresses where to send the TAN for transactions.
- contact_data?: ChallengeContactData;
-
- // 'payto' address of a fiat bank account.
- // Payments will be sent to this bank account
- // when the user wants to convert the regional currency
- // back to fiat currency outside bank.
- cashout_payto_uri?: PaytoString;
-
- // Internal payto URI of this bank account.
- // Used mostly for testing.
- payto_uri?: PaytoString;
-
- // If present, set the max debit allowed for this user
- // Only admin can set this property.
- debit_threshold?: AmountString;
-
- // If present, set a custom minimum cashout amount for this account.
- // Only admin can set this property
- // @since v4
- min_cashout?: AmountString;
-
- // If present, enables 2FA and set the TAN channel used for challenges
- // Only admin can set this property, other user can reconfig their account
- // after creation.
- tan_channel?: TanChannel;
- }
-
- export interface ChallengeContactData {
- // E-Mail address
- email?: EmailAddress;
-
- // Phone number.
- phone?: PhoneNumber;
- }
-
- export interface AccountReconfiguration {
- // Addresses where to send the TAN for transactions.
- // Currently only used for cashouts.
- // If missing, cashouts will fail.
- // In the future, might be used for other transactions
- // as well.
- // Only admin can change this property.
- contact_data?: ChallengeContactData;
-
- // 'payto' URI of a fiat bank account.
- // Payments will be sent to this bank account
- // when the user wants to convert the regional currency
- // back to fiat currency outside bank.
- // Only admin can change this property if not allowed in config
- cashout_payto_uri?: PaytoString;
-
- // If present, change the legal name associated with $username.
- // Only admin can change this property if not allowed in config
- name?: string;
-
- // Make this account visible to anyone?
- is_public?: boolean;
-
- // If present, change the max debit allowed for this user
- // Only admin can change this property.
- debit_threshold?: AmountString;
-
- // If present, change the custom minimum cashout amount for this account.
- // Only admin can set this property
- // @since v4
- min_cashout?: AmountString;
-
- // If present, enables 2FA and set the TAN channel used for challenges
- tan_channel?: TanChannel | null;
- }
-
- export interface AccountPasswordChange {
- // New password.
- new_password: string;
- // Old password. If present, check that the old password matches.
- // Optional for admin account.
- old_password?: string;
- }
-
- export interface PublicAccountsResponse {
- public_accounts: PublicAccount[];
- }
- export interface PublicAccount {
- // Username of the account
- username: string;
-
- // Internal payto URI of this bank account.
- payto_uri: string;
-
- // Current balance of the account
- balance: Balance;
-
- // Is this a taler exchange account?
- is_taler_exchange: boolean;
-
- // Opaque unique ID used for pagination.
- // @since v4, will become mandatory in the future.
- row_id?: Integer;
- }
-
- export interface ListBankAccountsResponse {
- accounts: AccountMinimalData[];
- }
- export interface Balance {
- amount: AmountString;
- credit_debit_indicator: "credit" | "debit";
- }
- export interface AccountMinimalData {
- // Username
- username: string;
-
- // Legal name of the account owner.
- name: string;
-
- // Internal payto URI of this bank account.
- payto_uri: PaytoString;
-
- // current balance of the account
- balance: Balance;
-
- // Number indicating the max debit allowed for the requesting user.
- debit_threshold: AmountString;
-
- // Custom minimum cashout amount for this account.
- // If null or absent, the global conversion fee is used.
- // @since v4
- min_cashout?: AmountString;
-
- // Is this account visible to anyone?
- is_public: boolean;
-
- // Is this a taler exchange account?
- is_taler_exchange: boolean;
-
- // Opaque unique ID used for pagination.
- // @since v4, will become mandatory in the future.
- row_id?: Integer;
-
- // Current status of the account
- // active: the account can be used
- // deleted: the account has been deleted but is retained for compliance
- // reasons, only the administrator can access it
- // Default to 'active' is missing
- // @since v4, will become mandatory in the next version.
- status?: "active" | "deleted";
- }
-
- export interface AccountData {
- // Legal name of the account owner.
- name: string;
-
- // Available balance on the account.
- balance: Balance;
-
- // payto://-URI of the account.
- payto_uri: PaytoString;
-
- // Number indicating the max debit allowed for the requesting user.
- debit_threshold: AmountString;
-
- // Custom minimum cashout amount for this account.
- // If null or absent, the global conversion fee is used.
- // @since v4
- min_cashout?: AmountString;
-
- contact_data?: ChallengeContactData;
-
- // 'payto' address pointing the bank account
- // where to send cashouts. This field is optional
- // because not all the accounts are required to participate
- // in the merchants' circuit. One example is the exchange:
- // that never cashouts. Registering these accounts can
- // be done via the access API.
- cashout_payto_uri?: PaytoString;
-
- // Is this account visible to anyone?
- is_public: boolean;
-
- // Is this a taler exchange account?
- is_taler_exchange: boolean;
-
- // Is 2FA enabled and what channel is used for challenges?
- tan_channel?: TanChannel;
-
- // Current status of the account
- // active: the account can be used
- // deleted: the account has been deleted but is retained for compliance
- // reasons, only the administrator can access it
- // Default to 'active' is missing
- // @since v4, will become mandatory in the next version.
- status?: "active" | "deleted";
- }
-
- export interface CashoutRequest {
- // Nonce to make the request idempotent. Requests with the same
- // request_uid that differ in any of the other fields
- // are rejected.
- request_uid: ShortHashCode;
-
- // Optional subject to associate to the
- // cashout operation. This data will appear
- // as the incoming wire transfer subject in
- // the user's fiat bank account.
- subject?: string;
-
- // That is the plain amount that the user specified
- // to cashout. Its $currency is the (regional) currency of the
- // bank instance.
- amount_debit: AmountString;
-
- // That is the amount that will effectively be
- // transferred by the bank to the user's bank
- // account, that is external to the regional currency.
- // It is expressed in the fiat currency and
- // is calculated after the cashout fee and the
- // exchange rate. See the /cashout-rates call.
- // The client needs to calculate this amount
- // correctly based on the amount_debit and the cashout rate,
- // otherwise the request will fail.
- amount_credit: AmountString;
- }
-
- export interface CashoutResponse {
- // ID identifying the operation being created
- cashout_id: number;
- }
-
- /**
- * @deprecated since 4, use 2fa
- */
- export interface CashoutConfirmRequest {
- // the TAN that confirms $CASHOUT_ID.
- tan: string;
- }
-
- export interface Cashouts {
- // Every string represents a cash-out operation ID.
- cashouts: CashoutInfo[];
- }
-
- export interface CashoutInfo {
- cashout_id: number;
- }
- export interface GlobalCashouts {
- // Every string represents a cash-out operation ID.
- cashouts: GlobalCashoutInfo[];
- }
- export interface GlobalCashoutInfo {
- cashout_id: number;
- username: string;
- }
-
- export interface CashoutStatusResponse {
- // Amount debited to the internal
- // regional currency bank account.
- amount_debit: AmountString;
-
- // Amount credited to the external bank account.
- amount_credit: AmountString;
-
- // Transaction subject.
- subject: string;
-
- // Time when the cashout was created.
- creation_time: Timestamp;
- }
-
- export interface ConversionRatesResponse {
- // Exchange rate to buy the local currency from the external one
- buy_at_ratio: DecimalNumber;
-
- // Exchange rate to sell the local currency for the external one
- sell_at_ratio: DecimalNumber;
-
- // Fee to subtract after applying the buy ratio.
- buy_in_fee: DecimalNumber;
-
- // Fee to subtract after applying the sell ratio.
- sell_out_fee: DecimalNumber;
- }
-
- export enum MonitorTimeframeParam {
- hour,
- day,
- month,
- year,
- decade,
- }
-
- export type MonitorResponse = MonitorNoConversion | MonitorWithConversion;
-
- // Monitoring stats when conversion is not supported
- export interface MonitorNoConversion {
- type: "no-conversions";
-
- // How many payments were made to a Taler exchange by another
- // bank account.
- talerInCount: number;
-
- // Overall volume that has been paid to a Taler
- // exchange by another bank account.
- talerInVolume: AmountString;
-
- // How many payments were made by a Taler exchange to another
- // bank account.
- talerOutCount: number;
-
- // Overall volume that has been paid by a Taler
- // exchange to another bank account.
- talerOutVolume: AmountString;
- }
- // Monitoring stats when conversion is supported
- export interface MonitorWithConversion {
- type: "with-conversions";
-
- // How many cashin operations were confirmed by a
- // wallet owner. Note: wallet owners
- // are NOT required to be customers of the libeufin-bank.
- cashinCount: number;
-
- // Overall regional currency that has been paid by the regional admin account
- // to regional bank accounts to fulfill all the confirmed cashin operations.
- cashinRegionalVolume: AmountString;
-
- // Overall fiat currency that has been paid to the fiat admin account
- // by fiat bank accounts to fulfill all the confirmed cashin operations.
- cashinFiatVolume: AmountString;
-
- // How many cashout operations were confirmed.
- cashoutCount: number;
-
- // Overall regional currency that has been paid to the regional admin account
- // by fiat bank accounts to fulfill all the confirmed cashout operations.
- cashoutRegionalVolume: AmountString;
-
- // Overall fiat currency that has been paid by the fiat admin account
- // to fiat bank accounts to fulfill all the confirmed cashout operations.
- cashoutFiatVolume: AmountString;
-
- // How many payments were made to a Taler exchange by another
- // bank account.
- talerInCount: number;
-
- // Overall volume that has been paid to a Taler
- // exchange by another bank account.
- talerInVolume: AmountString;
-
- // How many payments were made by a Taler exchange to another
- // bank account.
- talerOutCount: number;
-
- // Overall volume that has been paid by a Taler
- // exchange to another bank account.
- talerOutVolume: AmountString;
- }
- export interface TanTransmission {
- // Channel of the last successful transmission of the TAN challenge.
- tan_channel: TanChannel;
-
- // Info of the last successful transmission of the TAN challenge.
- tan_info: string;
- }
-
- export interface Challenge {
- // Unique identifier of the challenge to solve to run this protected
- // operation.
- challenge_id: number;
- }
-
- export interface ChallengeSolve {
- // The TAN code that solves $CHALLENGE_ID
- tan: string;
- }
-
- export enum TanChannel {
- SMS = "sms",
- EMAIL = "email",
- }
-}
-
-export namespace TalerExchangeApi {
- export enum AmlState {
- normal = 0,
- pending = 1,
- frozen = 2,
- }
-
- export interface AmlRecords {
- // Array of AML records matching the query.
- records: AmlRecord[];
- }
- export interface AmlRecord {
- // Which payto-address is this record about.
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the current AML state.
- current_state: AmlState;
-
- // Monthly transaction threshold before a review will be triggered
- threshold: AmountString;
-
- // RowID of the record.
- rowid: Integer;
- }
-
- export interface AmlDecisionDetails {
- // Array of AML decisions made for this account. Possibly
- // contains only the most recent decision if "history" was
- // not set to 'true'.
- aml_history: AmlDecisionDetail[];
-
- // Array of KYC attributes obtained for this account.
- kyc_attributes: KycDetail[];
- }
- export interface AmlDecisionDetail {
- // What was the justification given?
- justification: string;
-
- // What is the new AML state.
- new_state: Integer;
-
- // When was this decision made?
- decision_time: Timestamp;
-
- // What is the new AML decision threshold (in monthly transaction volume)?
- new_threshold: AmountString;
-
- // Who made the decision?
- decider_pub: AmlOfficerPublicKeyP;
- }
- export interface KycDetail {
- // Name of the configuration section that specifies the provider
- // which was used to collect the KYC details
- provider_section: string;
-
- // The collected KYC data. NULL if the attribute data could not
- // be decrypted (internal error of the exchange, likely the
- // attribute key was changed).
- attributes?: Object;
-
- // Time when the KYC data was collected
- collection_time: Timestamp;
-
- // Time when the validity of the KYC data will expire
- expiration_time: Timestamp;
- }
-
- export interface AmlDecision {
- // Human-readable justification for the decision.
- justification: string;
-
- // At what monthly transaction volume should the
- // decision be automatically reviewed?
- new_threshold: AmountString;
-
- // Which payto-address is the decision about?
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the new AML state (e.g. frozen, unfrozen, etc.)
- // Numerical values are defined in AmlDecisionState.
- new_state: Integer;
-
- // Signature by the AML officer over a
- // TALER_MasterAmlOfficerStatusPS.
- // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
- officer_sig: EddsaSignature;
-
- // When was the decision made?
- decision_time: Timestamp;
-
- // Optional argument to impose new KYC requirements
- // that the customer has to satisfy to unblock transactions.
- kyc_requirements?: string[];
- }
-
- export interface ExchangeVersionResponse {
- // libtool-style representation of the Exchange 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-exchange";
-
- // URN of the implementation (needed to interpret 'revision' in version).
- // @since v18, may become mandatory in the future.
- implementation?: string;
-
- // Currency supported by this exchange, given
- // as a currency code ("USD" or "EUR").
- currency: string;
-
- // How wallets should render this currency.
- currency_specification: CurrencySpecification;
-
- // Names of supported KYC requirements.
- supported_kyc_requirements: string[];
- }
-
- export type AccountRestriction =
- | RegexAccountRestriction
- | DenyAllAccountRestriction;
- // Account restriction that disables this type of
- // account for the indicated operation categorically.
- export interface DenyAllAccountRestriction {
- type: "deny";
- }
- // Accounts interacting with this type of account
- // restriction must have a payto://-URI matching
- // the given regex.
- export 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 };
- }
-
- export interface WireAccount {
- // payto:// URI identifying the account and wire method
- payto_uri: PaytoString;
-
- // 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;
- }
-
- export interface ExchangeKeysResponse {
- // libtool-style representation of the Exchange protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- version: string;
-
- // The exchange's base URL.
- base_url: string;
-
- // The exchange's currency or asset unit.
- currency: string;
-
- /**
- * FIXME: PARTIALLY IMPLEMENTED!!
- */
-
- // How wallets should render this currency.
- // currency_specification: CurrencySpecification;
-
- // // Absolute cost offset for the STEFAN curve used
- // // to (over) approximate fees payable by amount.
- // stefan_abs: AmountString;
-
- // // Factor to multiply the logarithm of the amount
- // // with to (over) approximate fees payable by amount.
- // // Note that the total to be paid is first to be
- // // divided by the smallest denomination to obtain
- // // the value that the logarithm is to be taken of.
- // stefan_log: AmountString;
-
- // // Linear cost factor for the STEFAN curve used
- // // to (over) approximate fees payable by amount.
- // //
- // // Note that this is a scalar, as it is multiplied
- // // with the actual amount.
- // stefan_lin: Float;
-
- // // Type of the asset. "fiat", "crypto", "regional"
- // // or "stock". Wallets should adjust their UI/UX
- // // based on this value.
- // asset_type: string;
-
- // // Array of wire accounts operated by the exchange for
- // // incoming wire transfers.
- // accounts: WireAccount[];
-
- // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank")
- // // to wire fees.
- // wire_fees: { method: AggregateTransferFee[] };
-
- // // List of exchanges that this exchange is partnering
- // // with to enable wallet-to-wallet transfers.
- // wads: ExchangePartner[];
-
- // // Set to true if this exchange allows the use
- // // of reserves for rewards.
- // // @deprecated in protocol v18.
- // rewards_allowed: false;
-
- // // EdDSA master public key of the exchange, used to sign entries
- // // in denoms and signkeys.
- // master_public_key: EddsaPublicKey;
-
- // // Relative duration until inactive reserves are closed;
- // // not signed (!), can change without notice.
- // reserve_closing_delay: RelativeTime;
-
- // // Threshold amounts beyond which wallet should
- // // trigger the KYC process of the issuing
- // // exchange. Optional option, if not given there is no limit.
- // // Currency must match currency.
- // wallet_balance_limit_without_kyc?: AmountString[];
-
- // // Denominations offered by this exchange
- // denominations: DenomGroup[];
-
- // // Compact EdDSA signature (binary-only) over the
- // // contatentation of all of the master_sigs (in reverse
- // // chronological order by group) in the arrays under
- // // "denominations". Signature of TALER_ExchangeKeySetPS
- // exchange_sig: EddsaSignature;
-
- // // Public EdDSA key of the exchange that was used to generate the signature.
- // // Should match one of the exchange's signing keys from signkeys. It is given
- // // explicitly as the client might otherwise be confused by clock skew as to
- // // which signing key was used for the exchange_sig.
- // exchange_pub: EddsaPublicKey;
-
- // // Denominations for which the exchange currently offers/requests recoup.
- // recoup: Recoup[];
-
- // // Array of globally applicable fees by time range.
- // global_fees: GlobalFees[];
-
- // // The date when the denomination keys were last updated.
- // list_issue_date: Timestamp;
-
- // // Auditors of the exchange.
- // auditors: AuditorKeys[];
-
- // // The exchange's signing keys.
- // signkeys: SignKey[];
-
- // // Optional field with a dictionary of (name, object) pairs defining the
- // // supported and enabled extensions, such as age_restriction.
- // extensions?: { name: ExtensionManifest };
-
- // // Signature by the exchange master key of the SHA-256 hash of the
- // // normalized JSON-object of field extensions, if it was set.
- // // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
- // extensions_sig?: EddsaSignature;
- }
-
- interface ExtensionManifest {
- // The criticality of the extension MUST be provided. It has the same
- // semantics as "critical" has for extensions in X.509:
- // - if "true", the client must "understand" the extension before
- // proceeding,
- // - if "false", clients can safely skip extensions they do not
- // understand.
- // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)
- critical: boolean;
-
- // The version information MUST be provided in Taler's protocol version
- // ranges notation, see
- // https://docs.taler.net/core/api-common.html#protocol-version-ranges
- version: LibtoolVersion;
-
- // Optional configuration object, defined by the feature itself
- config?: object;
- }
-
- interface SignKey {
- // The actual exchange's EdDSA signing public key.
- key: EddsaPublicKey;
-
- // Initial validity date for the signing key.
- stamp_start: Timestamp;
-
- // Date when the exchange will stop using the signing key, allowed to overlap
- // slightly with the next signing key's validity to allow for clock skew.
- stamp_expire: Timestamp;
-
- // Date when all signatures made by the signing key expire and should
- // henceforth no longer be considered valid in legal disputes.
- stamp_end: Timestamp;
-
- // Signature over key and stamp_expire by the exchange master key.
- // Signature of TALER_ExchangeSigningKeyValidityPS.
- // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY.
- master_sig: EddsaSignature;
- }
-
- interface AuditorKeys {
- // The auditor's EdDSA signing public key.
- auditor_pub: EddsaPublicKey;
-
- // The auditor's URL.
- auditor_url: string;
-
- // The auditor's name (for humans).
- auditor_name: string;
-
- // An array of denomination keys the auditor affirms with its signature.
- // Note that the message only includes the hash of the public key, while the
- // signature is actually over the expanded information including expiration
- // times and fees. The exact format is described below.
- denomination_keys: AuditorDenominationKey[];
- }
- interface AuditorDenominationKey {
- // Hash of the public RSA key used to sign coins of the respective
- // denomination. Note that the auditor's signature covers more than just
- // the hash, but this other information is already provided in denoms and
- // thus not repeated here.
- denom_pub_h: HashCode;
-
- // Signature of TALER_ExchangeKeyValidityPS.
- auditor_sig: EddsaSignature;
- }
-
- interface GlobalFees {
- // What date (inclusive) does these fees go into effect?
- start_date: Timestamp;
-
- // What date (exclusive) does this fees stop going into effect?
- end_date: Timestamp;
-
- // Account history fee, charged when a user wants to
- // obtain a reserve/account history.
- history_fee: AmountString;
-
- // Annual fee charged for having an open account at the
- // exchange. Charged to the account. If the account
- // balance is insufficient to cover this fee, the account
- // is automatically deleted/closed. (Note that the exchange
- // will keep the account history around for longer for
- // regulatory reasons.)
- account_fee: AmountString;
-
- // Purse fee, charged only if a purse is abandoned
- // and was not covered by the account limit.
- purse_fee: AmountString;
-
- // How long will the exchange preserve the account history?
- // After an account was deleted/closed, the exchange will
- // retain the account history for legal reasons until this time.
- history_expiration: RelativeTime;
-
- // Non-negative number of concurrent purses that any
- // account holder is allowed to create without having
- // to pay the purse_fee.
- purse_account_limit: Integer;
-
- // How long does an exchange keep a purse around after a purse
- // has expired (or been successfully merged)? A 'GET' request
- // for a purse will succeed until the purse expiration time
- // plus this value.
- purse_timeout: RelativeTime;
-
- // Signature of TALER_GlobalFeesPS.
- master_sig: EddsaSignature;
- }
-
- interface Recoup {
- // Hash of the public key of the denomination that is being revoked under
- // emergency protocol (see /recoup).
- h_denom_pub: HashCode;
-
- // We do not include any signature here, as the primary use-case for
- // this emergency involves the exchange having lost its signing keys,
- // so such a signature here would be pretty worthless. However, the
- // exchange will not honor /recoup requests unless they are for
- // denomination keys listed here.
- }
-
- interface AggregateTransferFee {
- // Per transfer wire transfer fee.
- wire_fee: AmountString;
-
- // Per transfer closing fee.
- closing_fee: AmountString;
-
- // 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;
- }
-
- interface ExchangePartner {
- // Base URL of the partner exchange.
- partner_base_url: string;
-
- // Public master key of the partner exchange.
- partner_master_pub: EddsaPublicKey;
-
- // Per exchange-to-exchange transfer (wad) fee.
- wad_fee: AmountString;
-
- // Exchange-to-exchange wad (wire) transfer frequency.
- wad_frequency: RelativeTime;
-
- // When did this partnership begin (under these conditions)?
- start_date: Timestamp;
-
- // How long is this partnership expected to last?
- end_date: Timestamp;
-
- // Signature using the exchange's offline key over
- // TALER_WadPartnerSignaturePS
- // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS.
- master_sig: EddsaSignature;
- }
-
- type DenomGroup =
- | DenomGroupRsa
- | DenomGroupCs
- | DenomGroupRsaAgeRestricted
- | DenomGroupCsAgeRestricted;
- interface DenomGroupRsa extends DenomGroupCommon {
- cipher: "RSA";
-
- denoms: ({
- rsa_pub: RsaPublicKey;
- } & DenomCommon)[];
- }
- interface DenomGroupCs extends DenomGroupCommon {
- cipher: "CS";
-
- denoms: ({
- cs_pub: Cs25519Point;
- } & DenomCommon)[];
- }
-
- // Binary representation of the age groups.
- // The bits set in the mask mark the edges at the beginning of a next age
- // group. F.e. for the age groups
- // 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-*
- // the following bits are set:
- //
- // 31 24 16 8 0
- // | | | | |
- // oooooooo oo1oo1o1 o1o1o1o1 ooooooo1
- //
- // A value of 0 means that the exchange does not support the extension for
- // age-restriction.
- type AgeMask = Integer;
-
- interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
- cipher: "RSA+age_restricted";
- age_mask: AgeMask;
-
- denoms: ({
- rsa_pub: RsaPublicKey;
- } & DenomCommon)[];
- }
- interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
- cipher: "CS+age_restricted";
- age_mask: AgeMask;
-
- denoms: ({
- cs_pub: Cs25519Point;
- } & DenomCommon)[];
- }
- // Common attributes for all denomination groups
- interface DenomGroupCommon {
- // How much are coins of this denomination worth?
- value: AmountString;
-
- // Fee charged by the exchange for withdrawing a coin of this denomination.
- fee_withdraw: AmountString;
-
- // Fee charged by the exchange for depositing a coin of this denomination.
- fee_deposit: AmountString;
-
- // Fee charged by the exchange for refreshing a coin of this denomination.
- fee_refresh: AmountString;
-
- // Fee charged by the exchange for refunding a coin of this denomination.
- fee_refund: AmountString;
- }
- interface DenomCommon {
- // Signature of TALER_DenominationKeyValidityPS.
- master_sig: EddsaSignature;
-
- // When does the denomination key become valid?
- stamp_start: Timestamp;
-
- // When is it no longer possible to withdraw coins
- // of this denomination?
- stamp_expire_withdraw: Timestamp;
-
- // When is it no longer possible to deposit coins
- // of this denomination?
- stamp_expire_deposit: Timestamp;
-
- // Timestamp indicating by when legal disputes relating to these coins must
- // be settled, as the exchange will afterwards destroy its evidence relating to
- // transactions involving this coin.
- stamp_expire_legal: Timestamp;
-
- // Set to 'true' if the exchange somehow "lost"
- // the private key. The denomination was not
- // necessarily revoked, but still cannot be used
- // to withdraw coins at this time (theoretically,
- // the private key could be recovered in the
- // future; coins signed with the private key
- // remain valid).
- lost?: boolean;
- }
- type DenominationKey = RsaDenominationKey | CSDenominationKey;
- interface RsaDenominationKey {
- cipher: "RSA";
-
- // 32-bit age mask.
- age_mask: Integer;
-
- // RSA public key
- rsa_public_key: RsaPublicKey;
- }
- interface CSDenominationKey {
- cipher: "CS";
-
- // 32-bit age mask.
- age_mask: Integer;
-
- // Public key of the denomination.
- cs_public_key: Cs25519Point;
- }
-}
-
-export namespace TalerMerchantApi {
- export 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";
-
- // URN of the implementation (needed to interpret 'revision' in version).
- // @since **v8**, may become mandatory in the future.
- implementation?: string;
-
- // Default (!) currency supported by this backend.
- // This is the currency that the backend should
- // suggest by default to the user when entering
- // amounts. See currencies for a list of
- // supported currencies and how to render them.
- currency: string;
-
- // How services should render currencies supported
- // by this backend. Maps
- // currency codes (e.g. "EUR" or "KUDOS") to
- // the respective currency specification.
- // All currencies in this map are supported by
- // the backend. Note that the actual currency
- // specifications are a *hint* for applications
- // that would like *advice* on how to render amounts.
- // Applications *may* ignore the currency specification
- // if they know how to render currencies that they are
- // used with.
- currencies: { [currency: string]: CurrencySpecification };
-
- // Array of exchanges trusted by the merchant.
- // Since protocol **v6**.
- exchanges: ExchangeConfigInfo[];
- }
-
- export interface ExchangeConfigInfo {
- // Base URL of the exchange REST API.
- base_url: string;
-
- // Currency for which the merchant is configured
- // to trust the exchange.
- // May not be the one the exchange actually uses,
- // but is the only one we would trust this exchange for.
- currency: string;
-
- // Offline master public key of the exchange. The
- // /keys data must be signed with this public
- // key for us to trust it.
- master_pub: EddsaPublicKey;
- }
- export interface ClaimRequest {
- // Nonce to identify the wallet that claimed the order.
- nonce: string;
-
- // Token that authorizes the wallet to claim the order.
- // *Optional* as the merchant may not have required it
- // (create_token set to false in PostOrderRequest).
- token?: ClaimToken;
- }
-
- export interface ClaimResponse {
- // Contract terms of the claimed order
- contract_terms: ContractTerms;
-
- // Signature by the merchant over the contract terms.
- sig: EddsaSignature;
- }
-
- export interface PaymentResponse {
- // Signature on TALER_PaymentResponsePS with the public
- // key of the merchant instance.
- sig: EddsaSignature;
-
- // Text to be shown to the point-of-sale staff as a proof of
- // payment.
- pos_confirmation?: string;
- }
-
- export interface PaymentStatusRequestParams {
- // Hash of the order’s contract terms (this is used to
- // authenticate the wallet/customer in case
- // $ORDER_ID is guessable).
- // Required once an order was claimed.
- contractTermHash?: string;
- // Authorizes the request via the claim token that
- // was returned in the PostOrderResponse. Used with
- // unclaimed orders only. Whether token authorization is
- // required is determined by the merchant when the
- // frontend creates the order.
- claimToken?: string;
- // Session ID that the payment must be bound to.
- // If not specified, the payment is not session-bound.
- sessionId?: string;
- // If specified, the merchant backend will wait up to
- // timeout_ms milliseconds for completion of the payment
- // before sending the HTTP response. A client must never
- // rely on this behavior, as the merchant backend may return
- // a response immediately.
- timeout?: number;
- // If set to “yes”, poll for the order’s pending refunds
- // to be picked up. timeout_ms specifies how long we
- // will wait for the refund.
- awaitRefundObtained?: boolean;
- // Indicates that we are polling for a refund above the
- // given AMOUNT. timeout_ms will specify how long we
- // will wait for the refund.
- refund?: AmountString;
- // Since protocol v9 refunded orders are only returned
- // under “already_paid_order_id” if this flag is set
- // explicitly to “YES”.
- allowRefundedForRepurchase?: boolean;
- }
- export interface GetKycStatusRequestParams {
- // If specified, the KYC check should return
- // the KYC status only for this wire account.
- // Otherwise, for all wire accounts.
- wireHash?: string;
- // If specified, the KYC check should return
- // the KYC status only for the given exchange.
- // Otherwise, for all exchanges we interacted with.
- exchangeURL?: string;
- // If specified, the merchant will wait up to
- // timeout_ms milliseconds for the exchanges to
- // confirm completion of the KYC process(es).
- timeout?: number;
- }
- export interface GetOtpDeviceRequestParams {
- // Timestamp in seconds to use when calculating
- // the current OTP code of the device. Since protocol v10.
- faketime?: number;
- // Price to use when calculating the current OTP
- // code of the device. Since protocol v10.
- price?: AmountString;
- }
- export interface GetOrderRequestParams {
- // Session ID that the payment must be bound to.
- // If not specified, the payment is not session-bound.
- sessionId?: string;
- // Timeout in milliseconds to wait for a payment if
- // the answer would otherwise be negative (long polling).
- timeout?: number;
- // Since protocol v9 refunded orders are only returned
- // under “already_paid_order_id” if this flag is set
- // explicitly to “YES”.
- allowRefundedForRepurchase?: boolean;
- }
- export interface ListWireTransferRequestParams {
- // Filter for transfers to the given bank account
- // (subject and amount MUST NOT be given in the payto URI).
- paytoURI?: string;
- // Filter for transfers executed before the given timestamp.
- before?: number;
- // Filter for transfers executed after the given timestamp.
- after?: number;
- // At most return the given number of results. Negative for
- // descending in execution time, positive for ascending in
- // execution time. Default is -20.
- limit?: number;
- // Starting transfer_serial_id for an iteration.
- offset?: string;
- // Filter transfers by verification status.
- verified?: boolean;
- order?: "asc" | "dec";
- }
- export interface ListOrdersRequestParams {
- // If set to yes, only return paid orders, if no only
- // unpaid orders. Do not give (or use “all”) to see all
- // orders regardless of payment status.
- paid?: boolean;
- // If set to yes, only return refunded orders, if no only
- // unrefunded orders. Do not give (or use “all”) to see
- // all orders regardless of refund status.
- refunded?: boolean;
- // If set to yes, only return wired orders, if no only
- // orders with missing wire transfers. Do not give (or
- // use “all”) to see all orders regardless of wire transfer
- // status.
- wired?: boolean;
- // At most return the given number of results. Negative
- // for descending by row ID, positive for ascending by
- // row ID. Default is 20. Since protocol v12.
- limit?: number;
- // Non-negative date in seconds after the UNIX Epoc, see delta
- // for its interpretation. If not specified, we default to the
- // oldest or most recent entry, depending on delta.
- date?: AbsoluteTime;
- // Starting product_serial_id for an iteration.
- // Since protocol v12.
- offset?: string;
- // Timeout in milliseconds to wait for additional orders if the
- // answer would otherwise be negative (long polling). Only useful
- // if delta is positive. Note that the merchant MAY still return
- // a response that contains fewer than delta orders.
- timeout?: number;
- // Since protocol v6. Filters by session ID.
- sessionId?: string;
- // Since protocol v6. Filters by fulfillment URL.
- fulfillmentUrl?: string;
-
- order?: "asc" | "dec";
- }
-
- export interface PayRequest {
- // The coins used to make the payment.
- coins: CoinPaySig[];
-
- // Custom inputs from the wallet for the contract.
- wallet_data?: Object;
-
- // The session for which the payment is made (or replayed).
- // Only set for session-based payments.
- session_id?: string;
- }
- export interface CoinPaySig {
- // Signature by the coin.
- coin_sig: EddsaSignature;
-
- // Public key of the coin being spent.
- coin_pub: EddsaPublicKey;
-
- // Signature made by the denomination public key.
- ub_sig: RsaSignature;
-
- // The hash of the denomination public key associated with this coin.
- h_denom: HashCode;
-
- // The amount that is subtracted from this coin with this payment.
- contribution: AmountString;
-
- // URL of the exchange this coin was withdrawn from.
- exchange_url: string;
- }
-
- export interface StatusPaid {
- type: "paid";
-
- // Was the payment refunded (even partially, via refund or abort)?
- refunded: boolean;
-
- // Is any amount of the refund still waiting to be picked up (even partially)?
- refund_pending: boolean;
-
- // Amount that was refunded in total.
- refund_amount: AmountString;
-
- // Amount that already taken by the wallet.
- refund_taken: AmountString;
- }
- export interface StatusGotoResponse {
- type: "goto";
- // The client should go to the reorder URL, there a fresh
- // order might be created as this one is taken by another
- // customer or wallet (or repurchase detection logic may
- // apply).
- public_reorder_url: string;
- }
- export interface StatusUnpaidResponse {
- type: "unpaid";
- // URI that the wallet must process to complete the payment.
- taler_pay_uri: string;
-
- // Status URL, can be used as a redirect target for the browser
- // to show the order QR code / trigger the wallet.
- fulfillment_url?: 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;
- }
-
- export interface PaidRefundStatusResponse {
- // Text to be shown to the point-of-sale staff as a proof of
- // payment (present only if reusable OTP algorithm is used).
- pos_confirmation?: string;
-
- // True if the order has been subjected to
- // refunds. False if it was simply paid.
- refunded: boolean;
- }
- export interface PaidRequest {
- // Signature on TALER_PaymentResponsePS with the public
- // key of the merchant instance.
- sig: EddsaSignature;
-
- // Hash of the order's contract terms (this is used to authenticate the
- // wallet/customer and to enable signature verification without
- // database access).
- h_contract: HashCode;
-
- // Hash over custom inputs from the wallet for the contract.
- wallet_data_hash?: HashCode;
-
- // Session id for which the payment is proven.
- session_id: string;
- }
-
- export interface AbortRequest {
- // Hash of the order's contract terms (this is used to authenticate the
- // wallet/customer in case $ORDER_ID is guessable).
- h_contract: HashCode;
-
- // List of coins the wallet would like to see refunds for.
- // (Should be limited to the coins for which the original
- // payment succeeded, as far as the wallet knows.)
- coins: AbortingCoin[];
- }
- interface AbortingCoin {
- // Public key of a coin for which the wallet is requesting an abort-related refund.
- coin_pub: EddsaPublicKey;
-
- // The amount to be refunded (matches the original contribution)
- contribution: AmountString;
-
- // URL of the exchange this coin was withdrawn from.
- exchange_url: string;
- }
- export interface AbortResponse {
- // List of refund responses about the coins that the wallet
- // requested an abort for. In the same order as the coins
- // from the original request.
- // The rtransaction_id is implied to be 0.
- refunds: MerchantAbortPayRefundStatus[];
- }
- export type MerchantAbortPayRefundStatus =
- | MerchantAbortPayRefundSuccessStatus
- | MerchantAbortPayRefundFailureStatus;
- // Details about why a refund failed.
- export interface MerchantAbortPayRefundFailureStatus {
- // Used as tag for the sum type RefundStatus sum type.
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: Integer;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: Integer;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: Object;
- }
- // Additional details needed to verify the refund confirmation signature
- // (h_contract_terms and merchant_pub) are already known
- // to the wallet and thus not included.
- export interface MerchantAbortPayRefundSuccessStatus {
- // Used as tag for the sum type MerchantCoinRefundStatus sum type.
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // The EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund.
- exchange_sig: EddsaSignature;
-
- // Public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKey;
- }
-
- export interface WalletRefundRequest {
- // Hash of the order's contract terms (this is used to authenticate the
- // wallet/customer).
- h_contract: HashCode;
- }
- export interface WalletRefundResponse {
- // Amount that was refunded in total.
- refund_amount: AmountString;
-
- // Successful refunds for this payment, empty array for none.
- refunds: MerchantCoinRefundStatus[];
-
- // Public key of the merchant.
- merchant_pub: EddsaPublicKey;
- }
- export type MerchantCoinRefundStatus =
- | MerchantCoinRefundSuccessStatus
- | MerchantCoinRefundFailureStatus;
- // Details about why a refund failed.
- export interface MerchantCoinRefundFailureStatus {
- // Used as tag for the sum type RefundStatus sum type.
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: Integer;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: Integer;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: Object;
-
- // Refund transaction ID.
- rtransaction_id: Integer;
-
- // Public key of a coin that was refunded.
- coin_pub: EddsaPublicKey;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- // Timestamp when the merchant approved the refund.
- // Useful for grouping refunds.
- execution_time: Timestamp;
- }
- // Additional details needed to verify the refund confirmation signature
- // (h_contract_terms and merchant_pub) are already known
- // to the wallet and thus not included.
- export interface MerchantCoinRefundSuccessStatus {
- // Used as tag for the sum type MerchantCoinRefundStatus sum type.
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // The EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund.
- exchange_sig: EddsaSignature;
-
- // Public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKey;
-
- // Refund transaction ID.
- rtransaction_id: Integer;
-
- // Public key of a coin that was refunded.
- coin_pub: EddsaPublicKey;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- // Timestamp when the merchant approved the refund.
- // Useful for grouping refunds.
- execution_time: Timestamp;
- }
-
- interface RewardInformation {
- // Exchange from which the reward will be withdrawn. Needed by the
- // wallet to determine denominations, fees, etc.
- exchange_url: string;
-
- // URL where to go after obtaining the reward.
- next_url: string;
-
- // (Remaining) amount of the reward (including fees).
- reward_amount: AmountString;
-
- // Timestamp indicating when the reward is set to expire (may be in the past).
- // Note that rewards that have expired MAY also result in a 404 response.
- expiration: Timestamp;
- }
-
- interface RewardPickupRequest {
- // List of planchets the wallet wants to use for the reward.
- planchets: PlanchetDetail[];
- }
- interface PlanchetDetail {
- // Hash of the denomination's public key (hashed to reduce
- // bandwidth consumption).
- denom_pub_hash: HashCode;
-
- // Coin's blinded public key.
- coin_ev: CoinEnvelope;
- }
- interface RewardResponse {
- // Blind RSA signatures over the planchets.
- // The order of the signatures matches the planchets list.
- blind_sigs: BlindSignature[];
- }
- interface BlindSignature {
- // The (blind) RSA signature. Still needs to be unblinded.
- blind_sig: BlindedRsaSignature;
- }
-
- export interface InstanceConfigurationMessage {
- // Name of the merchant instance to create (will become $INSTANCE).
- // Must match the regex ^[A-Za-z0-9][A-Za-z0-9_.@-]+$.
- 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?: string;
-
- // Merchant email for customer contact.
- email?: string;
-
- // Merchant public website.
- website?: string;
-
- // Merchant logo.
- logo?: ImageDataUrl;
-
- // Authentication settings for this instance
- 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;
- }
-
- export 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 "token", 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?: AccessToken;
- }
-
- export 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?: string;
-
- // 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;
- }
-
- export interface InstancesResponse {
- // List of instances that are present in the backend (see Instance).
- instances: Instance[];
- }
-
- export interface Instance {
- // Merchant name corresponding to this instance.
- name: string;
-
- // Type of the user ("business" or "individual").
- user_type: string;
-
- // 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;
- }
-
- export interface QueryInstancesResponse {
- // Merchant name corresponding to this instance.
- name: string;
-
- // Type of the user ("business" or "individual").
- user_type: string;
-
- // 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";
- };
- }
-
- export interface AccountKycRedirects {
- // Array of pending KYCs.
- pending_kycs: MerchantAccountKycRedirect[];
-
- // Array of exchanges with no reply.
- timeout_kycs: ExchangeKycTimeout[];
- }
-
- export 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;
-
- // AML status of the account.
- aml_status: Integer;
-
- // Base URL of the exchange this is about.
- exchange_url: string;
-
- // Our bank wire account this is about.
- payto_uri: PaytoString;
- }
-
- export 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;
- }
-
- export interface AccountAddDetails {
- // payto:// URI of the account.
- payto_uri: PaytoString;
-
- // 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;
- }
-
- export type FacadeCredentials =
- | NoFacadeCredentials
- | BasicAuthFacadeCredentials;
- export interface NoFacadeCredentials {
- type: "none";
- }
- export interface BasicAuthFacadeCredentials {
- type: "basic";
-
- // Username to use to authenticate
- username: string;
-
- // Password to use to authenticate
- password: string;
- }
- export interface AccountAddResponse {
- // Hash over the wire details (including over the salt).
- h_wire: HashCode;
-
- // Salt used to compute h_wire.
- salt: HashCode;
- }
-
- export 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".
- // If the argument is omitted, the old credentials
- // are simply preserved.
- credit_facade_credentials?: FacadeCredentials;
- }
-
- export interface AccountsSummaryResponse {
- // List of accounts that are known for the instance.
- accounts: BankAccountSummaryEntry[];
- }
-
- // TODO: missing in docs
- export interface BankAccountSummaryEntry {
- // payto:// URI of the account.
- payto_uri: PaytoString;
-
- // Hash over the wire details (including over the salt).
- h_wire: HashCode;
- }
- export interface BankAccountEntry {
- // payto:// URI of the account.
- payto_uri: PaytoString;
-
- // 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;
-
- // true if this account is active,
- // false if it is historic.
- active?: boolean;
- }
-
- export 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: AmountString;
-
- // 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;
- }
-
- export 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: AmountString;
-
- // 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;
- }
-
- export interface InventorySummaryResponse {
- // List of products that are present in the inventory.
- products: InventoryEntry[];
- }
-
- export interface InventoryEntry {
- // Product identifier, as found in the product.
- product_id: string;
- // product_serial_id of the product in the database.
- product_serial: Integer;
- }
-
- export interface FullInventoryDetailsResponse {
- // List of products that are present in the inventory.
- products: MerchantPosProductDetail[];
-
- // List of categories in the inventory.
- categories: MerchantCategory[];
- }
-
- export interface MerchantPosProductDetail {
- // A unique numeric ID of the product
- product_serial: number;
-
- // A merchant-internal unique identifier for the product
- product_id?: string;
-
- // A list of category IDs this product belongs to.
- // Typically, a product only belongs to one category, but more than one is supported.
- categories: number[];
-
- // 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: AmountString;
-
- // 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.
- // Optional, if missing treat as "infinite".
- total_stock?: Integer;
-
- // Minimum age buyer must have (in years).
- minimum_age?: Integer;
- }
-
- export interface MerchantCategory {
- // A unique numeric ID of the category
- id: number;
-
- // The name of the category. This will be shown to users and used in the order summary.
- name: string;
-
- // Map from IETF BCP 47 language tags to localized names
- name_i18n?: { [lang_tag: string]: string };
- }
-
- export 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: AmountString;
-
- // 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).
- minimum_age?: Integer;
- }
- export interface LockRequest {
- // UUID that identifies the frontend performing the lock
- // Must be unique for the lifetime of the lock.
- lock_uuid: string;
-
- // How long does the frontend intend to hold the lock?
- duration: RelativeTime;
-
- // How many units should be locked?
- quantity: Integer;
- }
-
- export 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
- // inventory_products is set. Used in case a frontend
- // reserved quantities of the individual products while
- // the shopping cart 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?: string[];
-
- // 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;
- }
-
- export type Order = MinimalOrderDetail & Partial<ContractTerms>;
-
- export interface MinimalOrderDetail {
- // Amount to be paid by the customer.
- amount: AmountString;
-
- // Short summary of the order.
- summary: string;
-
- // See documentation of fulfillment_url in ContractTerms.
- // Either fulfillment_url or fulfillment_message must be specified.
- // When creating an order, the fulfillment URL can
- // contain ${ORDER_ID} which will be substituted with the
- // order ID of the newly created order.
- fulfillment_url?: string;
-
- // See documentation of fulfillment_message in ContractTerms.
- // Either fulfillment_url or fulfillment_message must be specified.
- fulfillment_message?: string;
- }
-
- export interface MinimalInventoryProduct {
- // Which product is requested (here mandatory!).
- product_id: string;
-
- // How many units of the product are requested.
- quantity: Integer;
- }
-
- export 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;
- }
- export interface OutOfStockResponse {
- // Product ID of an out-of-stock item.
- product_id: string;
-
- // Requested quantity.
- requested_quantity: Integer;
-
- // Available quantity (must be below requested_quantity).
- available_quantity: Integer;
-
- // When do we expect the product to be again in stock?
- // Optional, not given if unknown.
- restock_expected?: Timestamp;
- }
-
- export interface OrderHistory {
- // Timestamp-sorted array of all orders matching the query.
- // The order of the sorting depends on the sign of delta.
- orders: OrderHistoryEntry[];
- }
- export 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: AmountString;
-
- // 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;
- }
-
- export type MerchantOrderStatusResponse =
- | CheckPaymentPaidResponse
- | CheckPaymentClaimedResponse
- | CheckPaymentUnpaidResponse;
- export 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: AmountString;
-
- // 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_code: 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_http_status: number;
-
- // Total amount that was refunded, 0 if refunded is false.
- refund_amount: AmountString;
-
- // 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;
- }
- export interface CheckPaymentClaimedResponse {
- // A wallet claimed the order, but did not yet pay for the contract.
- order_status: "claimed";
-
- // Contract terms.
- contract_terms: ContractTerms;
- }
- export interface CheckPaymentUnpaidResponse {
- // The order was neither claimed nor paid.
- order_status: "unpaid";
-
- // URI that the wallet must process to complete the payment.
- taler_pay_uri: string;
-
- // 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: AmountString;
-
- // 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.
- }
- export interface RefundDetails {
- // Reason given for the refund.
- reason: string;
-
- // Set to true if a refund is still available for the wallet for this payment.
- pending: boolean;
-
- // When was the refund approved.
- timestamp: Timestamp;
-
- // Total amount that was refunded (minus a refund fee).
- amount: AmountString;
- }
- export 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: AmountString;
-
- // Was this transfer confirmed by the merchant via the
- // POST /transfers API, or is it merely claimed by the exchange?
- confirmed: boolean;
- }
- export interface TransactionWireReport {
- // Numerical error code.
- code: number;
-
- // Human-readable error description.
- hint: string;
-
- // Numerical error code from the exchange.
- exchange_code: number;
-
- // HTTP status code received from the exchange.
- exchange_http_status: number;
-
- // Public key of the coin for which we got the exchange error.
- coin_pub: CoinPublicKey;
- }
-
- export interface ForgetRequest {
- // Array of valid JSON paths to forgettable fields in the order's
- // contract terms.
- fields: string[];
- }
-
- export interface RefundRequest {
- // Amount to be refunded.
- refund: AmountString;
-
- // Human-readable refund justification.
- reason: string;
- }
- export 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;
- }
-
- export interface TransferInformation {
- // How much was wired to the merchant (minus fees).
- credit_amount: AmountString;
-
- // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
- wtid: WireTransferIdentifierRawP;
-
- // Target account that received the wire transfer.
- payto_uri: PaytoString;
-
- // Base URL of the exchange that made the wire transfer.
- exchange_url: string;
- }
-
- export interface TransferList {
- // List of all the transfers that fit the filter that we know.
- transfers: TransferDetails[];
- }
- export interface TransferDetails {
- // How much was wired to the merchant (minus fees).
- credit_amount: AmountString;
-
- // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
- wtid: WireTransferIdentifierRawP;
-
- // Target account that received the wire transfer.
- payto_uri: PaytoString;
-
- // Base URL of the exchange that made the wire transfer.
- exchange_url: string;
-
- // Serial number identifying the transfer in the merchant backend.
- // Used for filtering 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;
- }
-
- export interface OtpDeviceAddDetails {
- // Device ID to use.
- otp_device_id: string;
-
- // Human-readable description for the device.
- otp_device_description: string;
-
- // A key encoded with RFC 3548 Base32.
- // IMPORTANT: This is not using the typical
- // Taler base32-crockford encoding.
- // Instead it uses the RFC 3548 encoding to
- // be compatible with the TOTP standard.
- otp_key: string;
-
- // Algorithm for computing the POS confirmation.
- // "NONE" or 0: No algorithm (no pos confirmation will be generated)
- // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device)
- // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device)
- // The "String" variants are supported @since protocol **v7**.
- otp_algorithm: Integer | string;
-
- // Counter for counter-based OTP devices.
- otp_ctr?: Integer;
- }
-
- export interface OtpDevicePatchDetails {
- // Human-readable description for the device.
- otp_device_description: string;
-
- // A key encoded with RFC 3548 Base32.
- // IMPORTANT: This is not using the typical
- // Taler base32-crockford encoding.
- // Instead it uses the RFC 3548 encoding to
- // be compatible with the TOTP standard.
- otp_key: string;
-
- // Algorithm for computing the POS confirmation.
- otp_algorithm: Integer;
-
- // Counter for counter-based OTP devices.
- otp_ctr?: Integer;
- }
-
- export interface OtpDeviceSummaryResponse {
- // Array of devices that are present in our backend.
- otp_devices: OtpDeviceEntry[];
- }
- export interface OtpDeviceEntry {
- // Device identifier.
- otp_device_id: string;
-
- // Human-readable description for the device.
- device_description: string;
- }
-
- export interface OtpDeviceDetails {
- // Human-readable description for the device.
- device_description: string;
-
- // Algorithm for computing the POS confirmation.
- //
- // Currently, the following numbers are defined:
- // 0: None
- // 1: TOTP without price
- // 2: TOTP with price
- otp_algorithm: Integer;
-
- // Counter for counter-based OTP devices.
- otp_ctr?: Integer;
-
- // Current time for time-based OTP devices.
- // Will match the faketime argument of the
- // query if one was present, otherwise the current
- // time at the backend.
- //
- // Available since protocol **v10**.
- otp_timestamp: Integer;
-
- // Current OTP confirmation string of the device.
- // Matches exactly the string that would be returned
- // as part of a payment confirmation for the given
- // amount and time (so may contain multiple OTP codes).
- //
- // If the otp_algorithm is time-based, the code is
- // returned for the current time, or for the faketime
- // if a TIMESTAMP query argument was provided by the client.
- //
- // When using OTP with counters, the counter is **NOT**
- // increased merely because this endpoint created
- // an OTP code (this is a GET request, after all!).
- //
- // If the otp_algorithm requires an amount, the
- // amount argument must be specified in the
- // query, otherwise the otp_code is not
- // generated.
- //
- // This field is *optional* in the response, as it is
- // only provided if we could compute it based on the
- // otp_algorithm and matching client query arguments.
- //
- // Available since protocol **v10**.
- otp_code?: string;
- }
- export 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;
-
- // Key-value pairs matching a subset of the
- // fields from template_contract that are
- // user-editable defaults for this template.
- // Since protocol **v13**.
- editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
- }
- export interface TemplateContractDetails {
- // Human-readable summary for the template.
- summary?: string;
-
- // Required currency for payments to the template.
- // The user may specify any amount, but it must be
- // in this currency.
- // This parameter is optional and should not be present
- // if "amount" is given.
- currency?: string;
-
- // The price is imposed by the merchant and cannot be changed by the customer.
- // This parameter is optional.
- amount?: AmountString;
-
- // 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;
- }
-
- export interface TemplateContractDetailsDefaults {
- summary?: string;
-
- currency?: string;
-
- /**
- * Amount *or* a plain currency string.
- */
- amount?: string;
- }
-
- export 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;
-
- // Key-value pairs matching a subset of the
- // fields from template_contract that are
- // user-editable defaults for this template.
- // Since protocol **v13**.
- editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
- }
-
- export interface TemplateSummaryResponse {
- // List of templates that are present in our backend.
- templates: TemplateEntry[];
- }
-
- export interface TemplateEntry {
- // Template identifier, as found in the template.
- template_id: string;
-
- // Human-readable description for the template.
- template_description: string;
- }
-
- export interface WalletTemplateDetails {
- // Hard-coded information about the contrac terms
- // for this template.
- template_contract: TemplateContractDetails;
-
- // Key-value pairs matching a subset of the
- // fields from template_contract that are
- // user-editable defaults for this template.
- // Since protocol **v13**.
- editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
- }
-
- export 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;
-
- // Key-value pairs matching a subset of the
- // fields from template_contract that are
- // user-editable defaults for this template.
- // Since protocol **v13**.
- editable_defaults?: TemplateContractDetailsDefaults;
-
- // Required currency for payments. Useful if no
- // amount is specified in the template_contract
- // but the user should be required to pay in a
- // particular currency anyway. Merchant backends
- // may reject requests if the template_contract
- // or editable_defaults do
- // specify an amount in a different currency.
- // This parameter is optional.
- // Since protocol **v13**.
- required_currency?: string;
- }
- export interface UsingTemplateDetails {
- // Summary of the template
- summary?: string;
-
- // The amount entered by the customer.
- amount?: AmountString;
- }
-
- export interface WebhookAddDetails {
- // Webhook ID to use.
- webhook_id: string;
-
- // 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;
- }
-
- export 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;
- }
-
- export interface WebhookSummaryResponse {
- // Return webhooks that are present in our backend.
- webhooks: WebhookEntry[];
- }
-
- export 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;
- }
-
- export 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;
- }
-
- export interface TokenFamilyCreateRequest {
- // Identifier for the token family consisting of unreserved characters
- // according to RFC 3986.
- slug: string;
-
- // Human-readable name for the token family.
- name: string;
-
- // Human-readable description for the token family.
- description: string;
-
- // Optional map from IETF BCP 47 language tags to localized descriptions.
- description_i18n?: { [lang_tag: string]: string };
-
- // Start time of the token family's validity period.
- // If not specified, merchant backend will use the current time.
- valid_after?: Timestamp;
-
- // End time of the token family's validity period.
- valid_before: Timestamp;
-
- // Validity duration of an issued token.
- duration: RelativeTime;
-
- // Kind of the token family.
- kind: TokenFamilyKind;
- }
-
- export enum TokenFamilyKind {
- Discount = "discount",
- Subscription = "subscription",
- }
-
- export interface TokenFamilyUpdateRequest {
- // Human-readable name for the token family.
- name: string;
-
- // Human-readable description for the token family.
- description: string;
-
- // Optional map from IETF BCP 47 language tags to localized descriptions.
- description_i18n: { [lang_tag: string]: string };
-
- // Start time of the token family's validity period.
- valid_after: Timestamp;
-
- // End time of the token family's validity period.
- valid_before: Timestamp;
-
- // Validity duration of an issued token.
- duration: RelativeTime;
- }
-
- export interface TokenFamiliesList {
- // All configured token families of this instance.
- token_families: TokenFamilySummary[];
- }
-
- export interface TokenFamilySummary {
- // Identifier for the token family consisting of unreserved characters
- // according to RFC 3986.
- slug: string;
-
- // Human-readable name for the token family.
- name: string;
-
- // Start time of the token family's validity period.
- valid_after: Timestamp;
-
- // End time of the token family's validity period.
- valid_before: Timestamp;
-
- // Kind of the token family.
- kind: TokenFamilyKind;
- }
-
- export interface TokenFamilyDetails {
- // Identifier for the token family consisting of unreserved characters
- // according to RFC 3986.
- slug: string;
-
- // Human-readable name for the token family.
- name: string;
-
- // Human-readable description for the token family.
- description: string;
-
- // Optional map from IETF BCP 47 language tags to localized descriptions.
- description_i18n?: { [lang_tag: string]: string };
-
- // Start time of the token family's validity period.
- valid_after: Timestamp;
-
- // End time of the token family's validity period.
- valid_before: Timestamp;
-
- // Validity duration of an issued token.
- duration: RelativeTime;
-
- // Kind of the token family.
- kind: TokenFamilyKind;
-
- // How many tokens have been issued for this family.
- issued: Integer;
-
- // How many tokens have been redeemed for this family.
- redeemed: Integer;
- }
- export 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: AmountString;
-
- // URL where the same contract could be ordered again (if
- // available). Returned also at the public order endpoint
- // for people other than the actual buyer (hence public,
- // in case order IDs are guessable).
- public_reorder_url?: 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).
- // Note that this placeholder can only be used once.
- // Either fulfillment_url or fulfillment_message must be specified.
- fulfillment_url?: string;
-
- // Message shown to the customer after paying for the order.
- // Either fulfillment_url or fulfillment_message must be specified.
- fulfillment_message?: string;
-
- // Map from IETF BCP 47 language tags to localized fulfillment
- // messages.
- fulfillment_message_i18n?: { [lang_tag: string]: string };
-
- // Maximum total deposit fee accepted by the merchant for this contract.
- // Overrides defaults of the merchant instance.
- max_fee: AmountString;
-
- // List of products that are part of the purchase (see Product).
- products: Product[];
-
- // Time when this contract was generated.
- timestamp: Timestamp;
-
- // After this deadline has passed, no refunds will be accepted.
- refund_deadline: Timestamp;
-
- // After this deadline, the merchant won't accept payments for the contract.
- pay_deadline: Timestamp;
-
- // Transfer deadline for the exchange. Must be in the
- // deposit permissions of coins used to pay for this order.
- wire_transfer_deadline: Timestamp;
-
- // 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;
-
- // 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?: Timestamp;
-
- // 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 the buyer must have (in years). Default is 0.
- // This value is at least as large as the maximum over all
- // minimum age requirements of the products in this contract.
- // It might also be set independent of any product, due to
- // legal requirements.
- minimum_age?: Integer;
- }
-
- export 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;
-
- // 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?: AmountString;
-
- // 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?: Timestamp;
- }
-
- export interface Tax {
- // The name of the tax.
- name: string;
-
- // Amount paid in tax.
- tax: AmountString;
- }
- export interface Merchant {
- // The merchant's legal name of business.
- name: string;
-
- // Label for a location with the business address of the merchant.
- email?: string;
-
- // Label for a location with the business address of the merchant.
- website?: string;
-
- // An optional base64-encoded product image.
- logo?: ImageDataUrl;
-
- // Label for a location with the business address of the merchant.
- address?: Location;
-
- // 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;
- }
- // Delivery location, loosely modeled as a subset of
- // ISO20022's PostalAddress25.
- export 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[];
- }
- interface Auditor {
- // Official name.
- name: string;
-
- // Auditor's public key.
- auditor_pub: EddsaPublicKey;
-
- // Base URL of the auditor.
- url: string;
- }
- export interface Exchange {
- // The exchange's base URL.
- url: string;
-
- // How much would the merchant like to use this exchange.
- // The wallet should use a suitable exchange with high
- // priority. The following priority values are used, but
- // it should be noted that they are NOT in any way normative.
- //
- // 0: likely it will not work (recently seen with account
- // restriction that would be bad for this merchant)
- // 512: merchant does not know, might be down (merchant
- // did not yet get /wire response).
- // 1024: good choice (recently confirmed working)
- priority: Integer;
-
- // Master public key of the exchange.
- master_pub: EddsaPublicKey;
- }
-
- export interface MerchantReserveCreateConfirmation {
- // Public key identifying the reserve.
- reserve_pub: EddsaPublicKey;
-
- // Wire accounts of the exchange where to transfer the funds.
- accounts: ExchangeWireAccount[];
- }
-
- export interface TemplateEditableDetails {
- // Human-readable summary for the template.
- summary?: string;
-
- // Required currency for payments to the template.
- // The user may specify any amount, but it must be
- // in this currency.
- // This parameter is optional and should not be present
- // if "amount" is given.
- currency?: string;
-
- // The price is imposed by the merchant and cannot be changed by the customer.
- // This parameter is optional.
- amount?: AmountString;
- }
-
- export interface MerchantTemplateContractDetails {
- // 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?: string;
-
- // Minimum age buyer must have (in years). Default is 0.
- minimum_age: number;
-
- // 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: TalerProtocolDuration;
- }
-
- export interface MerchantTemplateAddDetails {
- // Template ID to use.
- template_id: string;
-
- // Human-readable description for the template.
- template_description: string;
-
- // A base64-encoded image selected by the merchant.
- // This parameter is optional.
- // We are not sure about it.
- image?: string;
-
- editable_defaults?: TemplateEditableDetails;
-
- // Additional information in a separate template.
- template_contract: MerchantTemplateContractDetails;
-
- // OTP device ID.
- // This parameter is optional.
- otp_id?: string;
- }
-}
-
-export namespace ChallengerApi {
- export interface ChallengerTermsOfServiceResponse {
- // Name of the service
- name: "challenger";
-
- // libtool-style representation of the Challenger protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- version: string;
-
- // URN of the implementation (needed to interpret 'revision' in version).
- // @since v0, may become mandatory in the future.
- implementation?: string;
- }
-
- export interface ChallengeSetupResponse {
- // Nonce to use when constructing /authorize endpoint.
- nonce: string;
- }
-
- export interface Restriction {
- regex?: string;
- hint?: string;
- hint_i18n?: InternationalizedString;
- }
-
- export interface ChallengeStatus {
- // Object; map of keys (names of the fields of the address
- // to be entered by the user) to objects with a "regex" (string)
- // containing an extended Posix regular expression for allowed
- // address field values, and a "hint"/"hint_i18n" giving a
- // human-readable explanation to display if the value entered
- // by the user does not match the regex. Keys that are not mapped
- // to such an object have no restriction on the value provided by
- // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
- restrictions: Record<string, Restriction> | undefined;
-
- // indicates if the given address cannot be changed anymore, the
- // form should be read-only if set to true.
- fix_address: boolean;
-
- // form values from the previous submission if available, details depend
- // on the ADDRESS_TYPE, should be used to pre-populate the form
- last_address: Record<string, string> | undefined;
-
- // number of times the address can still be changed, may or may not be
- // shown to the user
- changes_left: Integer;
- }
-
- export interface ChallengeCreateResponse {
- // how many more attempts are allowed, might be shown to the user,
- // highlighting might be appropriate for low values such as 1 or 2 (the
- // form will never be used if the value is zero)
- attempts_left: Integer;
-
- // the address that is being validated, might be shown or not
- address: Object;
-
- // true if we just retransmitted the challenge, false if we sent a
- // challenge recently and thus refused to transmit it again this time;
- // might make a useful hint to the user
- transmitted: boolean;
-
- // timestamp explaining when we would re-transmit the challenge the next
- // time (at the earliest) if requested by the user
- next_tx_time: string;
- }
-
- export interface InvalidPinResponse {
- // numeric Taler error code, should be shown to indicate the error
- // compactly for reporting to developers
- ec?: number;
-
- // human-readable Taler error code, should be shown for the user to
- // understand the error
- hint: string;
-
- // how many times is the user still allowed to change the address;
- // if 0, the user should not be shown a link to jump to the
- // address entry form
- addresses_left: Integer;
-
- // how many times might the PIN still be retransmitted
- pin_transmissions_left: Integer;
-
- // how many times might the user still try entering the PIN code
- auth_attempts_left: Integer;
-
- // if true, the PIN was not even evaluated as the user previously
- // exhausted the number of attempts
- exhausted: boolean;
-
- // if true, the PIN was not even evaluated as no challenge was ever
- // issued (the user must have skipped the step of providing their
- // address first!)
- no_challenge: boolean;
- }
-
- export interface ChallengerAuthResponse {
- // Token used to authenticate access in /info.
- access_token: string;
-
- // Type of the access token.
- token_type: "Bearer";
-
- // Amount of time that an access token is valid (in seconds).
- expires_in: Integer;
- }
-
- export interface ChallengerInfoResponse {
- // Unique ID of the record within Challenger
- // (identifies the rowid of the token).
- id: Integer;
-
- // Address that was validated.
- // Key-value pairs, details depend on the
- // address_type.
- address: Object;
-
- // Type of the address.
- address_type: string;
-
- // How long do we consider the address to be
- // valid for this user.
- expires: Timestamp;
- }
-}
diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts
index bf186ce46..baea3ce6f 100644
--- a/packages/taler-util/src/http-client/utils.ts
+++ b/packages/taler-util/src/http-client/utils.ts
@@ -19,7 +19,11 @@
*/
import { base64FromArrayBuffer } from "../base64.js";
import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js";
-import { AccessToken, LongPollParams, PaginationParams } from "./types.js";
+import {
+ AccessToken,
+ LongPollParams,
+ PaginationParams,
+} from "../types-taler-common.js";
/**
* Helper function to generate the "Authorization" HTTP header.
@@ -99,17 +103,17 @@ export class IdempotencyRetry {
private constructor(timesLeft: number, maxTimesLeft: number) {
this.timesLeft = timesLeft;
this.maxTries = maxTimesLeft;
- this.uid = encodeCrock(getRandomBytes(32))
+ this.uid = encodeCrock(getRandomBytes(32));
}
-
+
static tryFiveTimes() {
- return new IdempotencyRetry(5, 5)
+ return new IdempotencyRetry(5, 5);
}
next(): IdempotencyRetry | undefined {
- const left = this.timesLeft -1
+ const left = this.timesLeft - 1;
if (left <= 0) {
- return undefined
+ return undefined;
}
return new IdempotencyRetry(left, this.maxTries);
}
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index d8cd36287..3f310e2b6 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -28,7 +28,7 @@ import {
import { Logger } from "./logging.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AbsoluteTime, Duration } from "./time.js";
-import { TalerErrorDetail } from "./wallet-types.js";
+import { TalerErrorDetail } from "./types-taler-wallet.js";
const textEncoder = new TextEncoder();
@@ -147,14 +147,15 @@ export async function readTalerErrorResponse(
let errJson;
try {
errJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from error response",
);
@@ -173,6 +174,7 @@ export async function readTalerErrorResponse(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
},
"Error response did not contain error code",
);
@@ -186,14 +188,15 @@ export async function readUnexpectedResponseDetails(
let errJson;
try {
errJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from error response",
);
@@ -206,6 +209,7 @@ export async function readUnexpectedResponseDetails(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
},
"Error response did not contain error code",
);
@@ -235,14 +239,15 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
let respJson;
try {
respJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from response",
);
@@ -250,14 +255,15 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
let parsedResponse: T;
try {
parsedResponse = codec.decode(respJson);
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Response invalid",
);
@@ -268,21 +274,22 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
};
}
-export async function readResponseJsonOrErrorCode<T>(
+export async function readResponseJsonOrThrow<T>(
httpResponse: HttpResponse,
codec: Codec<T>,
-): Promise<{ isError: boolean; response: T }> {
+): Promise<T> {
let respJson;
try {
respJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from response",
);
@@ -290,25 +297,22 @@ export async function readResponseJsonOrErrorCode<T>(
let parsedResponse: T;
try {
parsedResponse = codec.decode(respJson);
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Response invalid",
);
}
- return {
- isError: !(httpResponse.status >= 200 && httpResponse.status < 300),
- response: parsedResponse,
- };
+ return parsedResponse;
}
-
type HttpErrorDetails = {
requestUrl: string;
requestMethod: string;
@@ -329,14 +333,16 @@ export function throwUnexpectedRequestError(
httpResponse: HttpResponse,
talerErrorResponse: TalerErrorResponse,
): never {
+ const errorDetails = {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ };
+ logger.trace(`unexpected request error: ${j2s(errorDetails)}`);
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- {
- requestUrl: httpResponse.requestUrl,
- requestMethod: httpResponse.requestMethod,
- httpStatusCode: httpResponse.status,
- errorResponse: talerErrorResponse,
- },
+ errorDetails,
`Unexpected HTTP status ${httpResponse.status} in response`,
);
}
@@ -369,14 +375,15 @@ export async function readSuccessResponseTextOrErrorCode<T>(
let errJson;
try {
errJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from error response",
);
@@ -389,6 +396,7 @@ export async function readSuccessResponseTextOrErrorCode<T>(
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
+ response: await httpResponse.text(),
requestMethod: httpResponse.requestMethod,
},
"Error response did not contain error code",
@@ -413,14 +421,15 @@ export async function checkSuccessResponseOrThrow(
let errJson;
try {
errJson = await httpResponse.json();
- } catch (e: any) {
+ } catch (e) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
- validationError: e.toString(),
+ response: await httpResponse.text(),
+ validationError: e instanceof Error ? e.message : String(e),
},
"Couldn't parse JSON format from error response",
);
@@ -433,6 +442,7 @@ export async function checkSuccessResponseOrThrow(
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
+ response: await httpResponse.text(),
requestMethod: httpResponse.requestMethod,
},
"Error response did not contain error code",
@@ -484,7 +494,7 @@ export interface HttpLibArgs {
printAsCurl?: boolean;
}
-export function encodeBody(body: any): ArrayBuffer {
+export function encodeBody(body: unknown): ArrayBuffer {
if (body == null) {
return new ArrayBuffer(0);
}
@@ -494,6 +504,10 @@ export function encodeBody(body: any): ArrayBuffer {
return body.buffer;
} else if (body instanceof ArrayBuffer) {
return body;
+ } else if (body instanceof URLSearchParams) {
+ return textEncoder.encode(body.toString()).buffer;
+ } else if (typeof body === "object" && body.constructor.name === "FormData") {
+ return body as ArrayBuffer;
} else if (typeof body === "object") {
return textEncoder.encode(JSON.stringify(body)).buffer;
}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 45a12c258..9cc78f848 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -138,6 +138,10 @@ export class HttpLibImpl implements HttpRequestLibrary {
reqBody = encodeBody(opt.body);
}
+ if (opt?.body instanceof URLSearchParams) {
+ requestHeadersMap["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+
let path = parsedUrl.pathname;
if (parsedUrl.search != null) {
path += parsedUrl.search;
@@ -181,14 +185,14 @@ export class HttpLibImpl implements HttpRequestLibrary {
return arg + " '" + String(v) + "'";
}
console.log(
- `curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
+ `TALER_API_DEBUG: curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
"-d",
payload,
)}`,
);
}
- let timeoutHandle: NodeJS.Timer | undefined = undefined;
+ let timeoutHandle: NodeJS.Timeout | undefined = undefined;
let cancelCancelledHandler: (() => void) | undefined = undefined;
const doCleanup = () => {
@@ -236,6 +240,9 @@ export class HttpLibImpl implements HttpRequestLibrary {
},
};
doCleanup();
+ if (SHOW_CURL_HTTP_REQUEST) {
+ console.log(`TALER_API_DEBUG: ${res.statusCode} ${textDecoder.decode(data)}`)
+ }
resolve(resp);
});
res.on("error", (e) => {
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
index f60c82fc3..42a7f41e2 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -19,9 +19,9 @@
/**
* Imports.
*/
-import { Logger, openPromise } from "@gnu-taler/taler-util";
+import { j2s, Logger, openPromise } from "@gnu-taler/taler-util";
import { TalerError } from "./errors.js";
-import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js";
+import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
import {
Headers,
HttpRequestLibrary,
@@ -102,8 +102,8 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.headers) {
Object.entries(opt?.headers).forEach(([key, value]) => {
if (value === undefined) return;
- requestHeadersMap[key] = value
- })
+ requestHeadersMap[key] = value;
+ });
}
let headersList: string[] = [];
for (let headerName of Object.keys(requestHeadersMap)) {
@@ -115,13 +115,12 @@ export class HttpLibImpl implements HttpRequestLibrary {
const cancelPromCap = openPromise<QjsHttpResp>();
+ logger.trace(`calling qtart fetchHttp`);
+
// Just like WHATWG fetch(), the qjs http client doesn't
// really support cancellation, so cancellation here just
// means that the result is ignored!
- const {
- promise: fetchProm,
- cancelFn
- } = qjsOs.fetchHttp(url, {
+ const { promise: fetchProm, cancelFn } = qjsOs.fetchHttp(url, {
method,
data,
headers: headersList,
@@ -138,6 +137,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.cancellationToken) {
cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ logger.trace(`cancelling quickjs request`);
cancelFn();
cancelPromCap.reject(new RequestCancelledError());
});
@@ -147,6 +147,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
try {
res = await Promise.race([fetchProm, cancelPromCap.promise]);
} catch (e) {
+ logger.trace(`got exception while waiting for qtart http response`);
if (e instanceof RequestCancelledError) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
@@ -166,12 +167,17 @@ export class HttpLibImpl implements HttpRequestLibrary {
requestMethod: method,
httpStatusCode: 0,
},
- `Request timed out`,
+ `Request timed out (timeout ${opt?.timeout?.d_ms ?? "none"})`,
);
}
throw e;
}
+ if (logger.shouldLogTrace()) {
+ logger.trace(`got qtart http response, status ${res.status}`);
+ logger.trace(`response headers: ${j2s(res.headers)}`);
+ }
+
if (timeoutHandle != null) {
clearTimeout(timeoutHandle);
}
@@ -189,7 +195,16 @@ export class HttpLibImpl implements HttpRequestLibrary {
continue;
}
const headerName = headerStr.slice(0, splitPos).trim().toLowerCase();
- const headerValue = headerStr.slice(splitPos + 1).trim();
+ let headerValue = headerStr.slice(splitPos + 1).trim();
+ // FIXME: This is a hotfix for the broken native networking implementation on Android
+ // that sends the content type header value in square brackets
+ if (
+ headerName === "content-type" &&
+ headerValue.startsWith("[") &&
+ headerValue.endsWith("]")
+ ) {
+ headerValue = headerValue.substring(1, headerValue.length - 2);
+ }
headers.set(headerName, headerValue);
}
}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 9f99f2f5a..6111a601d 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -2,17 +2,11 @@ import { TalerErrorCode } from "./taler-error-codes.js";
export { TalerErrorCode };
-export * from "./CancellationToken.js";
-export * from "./MerchantApiClient.js";
-export { RequestThrottler } from "./RequestThrottler.js";
-export * from "./ReserveStatus.js";
-export * from "./ReserveTransaction.js";
-export { TaskThrottler } from "./TaskThrottler.js";
export * from "./amounts.js";
-export * from "./backup-types.js";
export * from "./bank-api-client.js";
export * from "./base64.js";
export * from "./bitcoin.js";
+export * from "./CancellationToken.js";
export * from "./codec.js";
export * from "./contract-terms.js";
export * from "./errors.js";
@@ -28,16 +22,15 @@ export * from "./http-client/challenger.js";
export * from "./http-client/exchange.js";
export * from "./http-client/merchant.js";
export * from "./http-client/officer-account.js";
-export * from "./http-client/types.js";
export { CacheEvictor } from "./http-client/utils.js";
export * from "./http-status-codes.js";
export * from "./i18n.js";
export * from "./iban.js";
export * from "./invariants.js";
export * from "./kdf.js";
-export * from "./libeufin-api-types.js";
export * from "./libtool-version.js";
export * from "./logging.js";
+export * from "./MerchantApiClient.js";
export {
crypto_sign_keyPair_fromSeed,
randomBytes,
@@ -50,13 +43,38 @@ export * from "./observability.js";
export * from "./operation.js";
export * from "./payto.js";
export * from "./promises.js";
+export * from "./qr.js";
+export { RequestThrottler } from "./RequestThrottler.js";
+export * from "./ReserveStatus.js";
+export * from "./ReserveTransaction.js";
export * from "./rfc3548.js";
export * from "./taler-crypto.js";
-export * from "./taler-types.js";
export * from "./taleruri.js";
+export { TaskThrottler } from "./TaskThrottler.js";
export * from "./time.js";
export * from "./timer.js";
export * from "./transaction-test-data.js";
-export * from "./transactions-types.js";
export * from "./url.js";
-export * from "./wallet-types.js";
+
+export * from "./types-taler-bank-conversion.js";
+export * from "./types-taler-bank-integration.js";
+export * from "./types-taler-common.js";
+export * from "./types-taler-corebank.js";
+export * from "./types-taler-exchange.js";
+export * from "./types-taler-merchant.js";
+export * from "./types-taler-sync.js";
+export * from "./types-taler-wallet-transactions.js";
+export * from "./types-taler-wallet.js";
+
+export * as TalerBankConversionApi from "./types-taler-bank-conversion.js";
+export * as TalerBankIntegrationApi from "./types-taler-bank-integration.js";
+export * as ChallengerApi from "./types-taler-challenger.js";
+export * as TalerCorebankApi from "./types-taler-corebank.js";
+export * as TalerExchangeApi from "./types-taler-exchange.js";
+export * as TalerMerchantApi from "./types-taler-merchant.js";
+export * as TalerRevenueApi from "./types-taler-revenue.js";
+export * as TalerWireGatewayApi from "./types-taler-wire-gateway.js";
+
+export * from "./taler-signatures.js";
+
+export * from "./account-restrictions.js";
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index a8a8c3299..c3e0070bc 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -23,8 +23,12 @@
* Imports.
*/
import { AbsoluteTime } from "./time.js";
-import { TransactionState } from "./transactions-types.js";
-import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
+import { TransactionState } from "./types-taler-wallet-transactions.js";
+import {
+ ExchangeEntryState,
+ TalerErrorDetail,
+ TransactionIdStr,
+} from "./types-taler-wallet.js";
export enum NotificationType {
BalanceChange = "balance-change",
@@ -134,6 +138,12 @@ export enum ObservabilityEventType {
CryptoFinishSuccess = "crypto-finish-success",
CryptoFinishError = "crypto-finish-error",
Message = "message",
+ /**
+ * Declare that an observability event is relevant to a particular transaction.
+ * If emitted from a request/task, all past/future events for that request/task
+ * should be shown for the transaction as well.
+ */
+ DeclareConcernsTransaction = "declare-concerns-transaction",
}
export type ObservabilityEvent =
@@ -171,6 +181,7 @@ export type ObservabilityEvent =
type: ObservabilityEventType.DbQueryFinishError;
name: string;
location: string;
+ error: TalerErrorDetail;
}
| {
type: ObservabilityEventType.RequestStart;
@@ -217,6 +228,10 @@ export type ObservabilityEvent =
| {
type: ObservabilityEventType.Message;
contents: string;
+ }
+ | {
+ type: ObservabilityEventType.DeclareConcernsTransaction;
+ transactionId: TransactionIdStr;
};
export interface BackupOperationErrorNotification {
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
index e2ab9d4e4..d757fd529 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -19,7 +19,7 @@
*/
import {
HttpResponse,
- readResponseJsonOrErrorCode,
+ readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "./http-common.js";
@@ -31,14 +31,14 @@ import {
TalerErrorDetail,
} from "./index.js";
-type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> =
- ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never;
+// type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> =
+// ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never;
export type OperationResult<Body, ErrorEnum, K = never> =
| OperationOk<Body>
| OperationAlternative<ErrorEnum, any>
| OperationFail<ErrorEnum>
- | OperationFailWithBodyOrNever<ErrorEnum, K>;
+// | OperationFailWithBodyOrNever<ErrorEnum, K>;
export function isOperationOk<T, E>(
c: OperationResult<T, E>,
@@ -75,7 +75,7 @@ export interface OperationFail<T> {
*/
case: T;
- detail: TalerErrorDetail;
+ detail?: TalerErrorDetail;
}
/**
@@ -88,12 +88,12 @@ export interface OperationAlternative<T, B> {
body: B;
}
-export interface OperationFailWithBody<B> {
- type: "fail";
+// export interface OperationFailWithBody<B> {
+// type: "fail";
- case: keyof B;
- body: B[OperationFailWithBody<B>["case"]];
-}
+// case: keyof B;
+// body: B[OperationFailWithBody<B>["case"]];
+// }
export async function opSuccessFromHttp<T>(
resp: HttpResponse,
@@ -115,10 +115,15 @@ export function opEmptySuccess(resp: HttpResponse): OperationOk<void> {
return { type: "ok" as const, body: void 0 };
}
-export async function opKnownFailureWithBody<B>(
- case_: keyof B,
- body: B[typeof case_],
-): Promise<OperationFailWithBody<B>> {
+export async function opKnownFailure<T>(
+ case_: T): Promise<OperationFail<T>> {
+ return { type: "fail", case: case_ };
+}
+
+export async function opKnownFailureWithBody<T,B>(
+ case_: T,
+ body: B,
+): Promise<OperationAlternative<T, B>> {
return { type: "fail", case: case_, body };
}
@@ -127,7 +132,7 @@ export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>(
s: T,
codec: Codec<B>,
): Promise<OperationAlternative<T, B>> {
- const body = (await readResponseJsonOrErrorCode(resp, codec)).response;
+ const body = await readResponseJsonOrThrow(resp, codec);
return { type: "fail", case: s, body };
}
@@ -146,7 +151,10 @@ export function opKnownTalerFailure<T extends TalerErrorCode>(
return { type: "fail", case: s, detail };
}
-export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
+export function opUnknownFailure(
+ resp: HttpResponse,
+ error: TalerErrorDetail,
+): never {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
@@ -179,6 +187,66 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
}
}
+export async function succeedOrThrow<R>(
+ promise: Promise<OperationResult<R, unknown>>,
+): Promise<R> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ return resp.body;
+ }
+
+ if (isOperationFail(resp)) {
+ throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any);
+ }
+ throw TalerError.fromException(resp);
+}
+
+export async function alternativeOrThrow<Error,Body, Alt>(
+ s: Error,
+ promise: Promise<OperationOk<Body>
+ | OperationAlternative<Error, Alt>
+ | OperationFail<Error>>,
+): Promise<Alt> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ throw TalerError.fromException(
+ new Error(`request succeed but failure "${s}" was expected`),
+ );
+ }
+ if (isOperationFail(resp) && resp.case !== s) {
+ throw TalerError.fromException(
+ new Error(
+ `request failed with "${JSON.stringify(
+ resp,
+ )}" but case "${s}" was expected`,
+ ),
+ );
+ }
+ return (resp as any).body;
+}
+
+export async function failOrThrow<E>(
+ s: E,
+ promise: Promise<OperationResult<unknown, E>>,
+): Promise<TalerErrorDetail | undefined> {
+ const resp = await promise;
+ if (isOperationOk(resp)) {
+ throw TalerError.fromException(
+ new Error(`request succeed but failure "${s}" was expected`),
+ );
+ }
+ if (isOperationFail(resp) && resp.case === s) {
+ return resp.detail;
+ }
+ throw TalerError.fromException(
+ new Error(
+ `request failed with "${JSON.stringify(
+ resp,
+ )}" but case "${s}" was expected`,
+ ),
+ );
+}
+
export type ResultByMethod<
TT extends object,
p extends keyof TT,
@@ -195,4 +263,4 @@ export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
OperationOk<any>
>;
-export type RedirectResult = { redirectURL: URL }
+export type RedirectResult = { redirectURL: URL };
diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts
index 66a05b3a2..3b28c4714 100644
--- a/packages/taler-util/src/payto.test.ts
+++ b/packages/taler-util/src/payto.test.ts
@@ -16,7 +16,7 @@
import test from "ava";
-import { parsePaytoUri } from "./payto.js";
+import { PaytoString, addPaytoQueryParams, parsePaytoUri, stringifyPaytoUri } from "./payto.js";
test("basic payto parsing", (t) => {
const r1 = parsePaytoUri("https://example.com/");
@@ -29,3 +29,28 @@ test("basic payto parsing", (t) => {
t.is(r3?.targetType, "x-taler-bank");
t.is(r3?.targetPath, "123");
});
+
+test("parsing payto and stringify again", (t) => {
+ const payto1 =
+ "payto://iban/DE1231231231?reciever-name=John%20Doe" as PaytoString;
+
+ t.is(stringifyPaytoUri(parsePaytoUri(payto1)!), payto1);
+});
+test("parsing payto with % carh", (t) => {
+ const payto1 =
+ "payto://iban/DE7763544441436?receiver-name=Test%20123%2B-%24%25%5E%3Cem%3Ehi%3C%2Fem%3E" as PaytoString;
+
+ t.is(stringifyPaytoUri(parsePaytoUri(payto1)!), payto1);
+});
+
+test("adding payto query params", (t) => {
+ const payto1 =
+ "payto://iban/DE1231231231?receiver-name=John%20Doe" as PaytoString;
+ const out1 = addPaytoQueryParams(payto1, {});
+ t.deepEqual(payto1, out1);
+
+ const out2 = addPaytoQueryParams(payto1, {
+ foo: "42",
+ });
+ t.deepEqual(out2, "payto://iban/DE1231231231?receiver-name=John%20Doe&foo=42");
+});
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 39c25cffd..3c28a9ad0 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,7 +15,21 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
-import { Codec, Context, DecodingError, buildCodecForObject, codecForStringURL, renderContext } from "./codec.js";
+import {
+ Codec,
+ Context,
+ DecodingError,
+ buildCodecForObject,
+ codecForStringURL,
+ renderContext,
+} from "./codec.js";
+import {
+ AccessToken,
+ codecForAccessToken,
+ codecOptional,
+ hashTruncate32,
+ stringToBytes,
+} from "./index.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -24,8 +38,9 @@ export type PaytoUri =
| PaytoUriTalerBank
| PaytoUriBitcoin;
-declare const __payto_str: unique symbol;
-export type PaytoString = string & { [__payto_str]: true };
+// declare const __payto_str: unique symbol;
+// export type PaytoString = string & { [__payto_str]: true };
+export type PaytoString = string;
export function codecForPaytoString(): Codec<PaytoString> {
return {
@@ -84,21 +99,25 @@ export function buildPayto(
type: "iban",
iban: string,
bic: string | undefined,
+ params?: Record<string, string>,
): PaytoUriIBAN;
export function buildPayto(
type: "bitcoin",
address: string,
reserve: string | undefined,
+ params?: Record<string, string>,
): PaytoUriBitcoin;
export function buildPayto(
type: "x-taler-bank",
host: string,
account: string,
+ params?: Record<string, string>,
): PaytoUriTalerBank;
export function buildPayto(
type: PaytoType,
first: string,
second?: string,
+ params: Record<string, string> = {},
): PaytoUriGeneric {
switch (type) {
case "bitcoin": {
@@ -108,7 +127,7 @@ export function buildPayto(
targetType: "bitcoin",
targetPath: first,
address: uppercased,
- params: {},
+ params,
segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first),
};
return result;
@@ -119,7 +138,7 @@ export function buildPayto(
isKnown: true,
targetType: "iban",
iban: uppercased,
- params: {},
+ params,
targetPath: !second ? uppercased : `${second}/${uppercased}`,
};
return result;
@@ -131,7 +150,7 @@ export function buildPayto(
targetType: "x-taler-bank",
host: first,
account: second,
- params: {},
+ params,
targetPath: `${first}/${second}`,
};
return result;
@@ -144,7 +163,9 @@ export function buildPayto(
}
/**
- * Add query parameters to a payto URI
+ * Add query parameters to a payto URI.
+ *
+ * Existing parameters are preserved.
*/
export function addPaytoQueryParams(
s: string,
@@ -152,14 +173,35 @@ export function addPaytoQueryParams(
): string {
const [acct, search] = s.slice(paytoPfx.length).split("?");
const searchParams = new URLSearchParams(search || "");
- const keys = Object.keys(params);
- if (keys.length === 0) {
- return paytoPfx + acct;
+ for (const [paramKey, paramValue] of Object.entries(params)) {
+ searchParams.set(paramKey, paramValue);
}
- for (const k of keys) {
- searchParams.set(k, params[k]);
+ const paramList = [...searchParams.entries()];
+ if (paramList.length === 0) {
+ return paytoPfx + acct;
}
- return paytoPfx + acct + "?" + searchParams.toString();
+ return paytoPfx + acct + "?" + createSearchParams(paramList);
+}
+
+/**
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
+ */
+function encodeRFC3986URIComponent(str: string): string {
+ return encodeURIComponent(str).replace(
+ /[!'()*]/g,
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
+ );
+}
+const rfc3986 = encodeRFC3986URIComponent;
+
+/**
+ *
+ * https://www.rfc-editor.org/rfc/rfc3986
+ */
+function createSearchParams(paramList: [string, string][]): string {
+ return paramList
+ .map(([key, value]) => `${rfc3986(key)}=${rfc3986(value)}`)
+ .join("&");
}
/**
@@ -171,12 +213,44 @@ export function addPaytoQueryParams(
export function stringifyPaytoUri(p: PaytoUri): PaytoString {
const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
const paramList = !p.params ? [] : Object.entries(p.params);
- paramList.forEach(([key, value]) => {
- url.searchParams.set(key, value);
- });
+ url.search = createSearchParams(paramList);
return url.href as PaytoString;
}
+export function hashPaytoUri(p: PaytoUri | string): Uint8Array {
+ const paytoUri = typeof p === "string" ? p : stringifyPaytoUri(p);
+ return hashTruncate32(stringToBytes(paytoUri + "\0"));
+}
+
+export function stringifyReservePaytoUri(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let target: string;
+ let domainWithOptPort: string;
+ if (url.protocol === "https:") {
+ target = "taler-reserve";
+ if (url.port != "443" && url.port !== "") {
+ domainWithOptPort = `${url.hostname}:${url.port}`;
+ } else {
+ domainWithOptPort = `${url.hostname}`;
+ }
+ } else {
+ target = "taler-reserve-http";
+ if (url.port != "80" && url.port !== "") {
+ domainWithOptPort = `${url.hostname}:${url.port}`;
+ } else {
+ domainWithOptPort = `${url.hostname}`;
+ }
+ }
+ let optPath = "";
+ if (url.pathname !== "/" && url.pathname !== "") {
+ optPath = url.pathname;
+ }
+ return `payto://${target}/${domainWithOptPort}${optPath}/${reservePub}`;
+}
+
/**
* Parse a valid payto:// uri into a PaytoUri object
* RFC 8905
@@ -205,7 +279,8 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
const searchParams = new URLSearchParams(search || "");
searchParams.forEach((v, k) => {
- params[k] = v;
+ // URLSearchParams already decodes uri components
+ params[k] = v; //decodeURIComponent(v);
});
if (targetType === "x-taler-bank") {
@@ -294,18 +369,19 @@ export function talerPaytoFromExchangeReserve(
/**
* The account letter is all the information
- * the merchant backend requires from the
+ * the merchant backend requires from the
* bank account to check transfer.
- *
+ *
*/
export type AccountLetter = {
accountURI: PaytoString;
infoURL: string;
+ accountToken?: AccessToken;
};
-export const codecForAccountLetter =
- (): Codec<AccountLetter> =>
- buildCodecForObject<AccountLetter>()
- .property("infoURL", codecForStringURL(true))
- .property("accountURI", codecForPaytoString())
- .build("AccountLetter");
+export const codecForAccountLetter = (): Codec<AccountLetter> =>
+ buildCodecForObject<AccountLetter>()
+ .property("infoURL", codecForStringURL(true))
+ .property("accountURI", codecForPaytoString())
+ .property("accountToken", codecOptional(codecForAccessToken()))
+ .build("AccountLetter");
diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts
new file mode 100644
index 000000000..4d90ccf14
--- /dev/null
+++ b/packages/taler-util/src/qr.ts
@@ -0,0 +1,166 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { Amounts } from "./index.js";
+import { parsePaytoUri } from "./payto.js";
+
+type EncodeResult = { type: "ok"; qrContent: string } | { type: "skip" };
+
+/**
+ * See "Schweizer Implementation Guidelines QR-Rechnung".
+ */
+function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult {
+ const parsedPayto = parsePaytoUri(paytoUri);
+ if (!parsedPayto) {
+ throw Error("invalid payto URI");
+ }
+ if (parsedPayto.targetType !== "iban") {
+ return { type: "skip" };
+ }
+ const amountStr = parsedPayto.params["amount"];
+ if (amountStr === undefined) {
+ return { type: "skip" };
+ }
+ const iban = parsedPayto.targetPath;
+ const countryCode = iban.slice(0, 2);
+ const lines = [
+ "SPC", // QRType
+ "0200", // Version
+ "1", // Character set (1: UTF-8)
+ iban, // Beneficiary IBAN
+ // Group: Beneficiary
+ "S", // Address type (S: structured)
+ parsedPayto.params["receiver-name"], // Beneficiary name
+ "", // street
+ "", // apt. nr.
+ parsedPayto.params["receiver-postal-code"], // town, // postal code
+ parsedPayto.params["receiver-town"], // town
+ countryCode, // Country
+ // Group: Ultimate Debtor (not used in version 0200)
+ "", // Ultimate Debtor Address type (S: structured)
+ "", // Ultimate Debtor name
+ "", // Ultimate Debtor street
+ "", // Ultimate Debtor apt. nr
+ "", // Ultimate Debtor postal code
+ "", // Ultimate Debtor town
+ "", // Ultimate Debtor country
+ // Group: Amount
+ Amounts.stringifyValue(amountStr, 2), // Amount
+ Amounts.currencyOf(amountStr), // Currency
+ // Group: Debtor
+ "", // Address type (S: structured)
+ "", // Debtor name
+ "", // Debtor street
+ "", // Debtor apt. nr
+ "", // Debtor postal code
+ "", // Debtor town
+ "", // Debtor country
+ // Group: Reference
+ "NON", // reference type
+ "", // Reference
+ // Group: Additional Information
+ parsedPayto.params["message"], // Unstructured data
+ "EPD", // End of payment data
+ ];
+
+ return {
+ type: "ok",
+ qrContent: lines.join("\n"),
+ };
+}
+
+/**
+ * See "Quick Response Code - Guidelines to
+ * Enable the Data Capture for the
+ * Initiation of a SEPA Credit Transfer".
+ */
+function encodePaytoAsEpcQr(paytoUri: string): EncodeResult {
+ const parsedPayto = parsePaytoUri(paytoUri);
+ if (!parsedPayto) {
+ throw Error("invalid payto URI");
+ }
+ if (parsedPayto.targetType !== "iban") {
+ return { type: "skip" };
+ }
+ const amountStr = parsedPayto.params["amount"];
+ Amounts.stringifyValue;
+ const lines = [
+ "BCD", // service tag
+ "002", // version
+ "1", // character set (1: UTF-8)
+ "SCT", // Identification
+ "", // optional BIC
+ parsedPayto.params["receiver-name"], // Beneficiary name
+ parsedPayto.targetPath, // Beneficiary IBAN
+ amountStr !== undefined
+ ? `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`
+ : "", // Amount (optional)
+ "", // AT-44 Purpose
+ parsedPayto.params["message"], // AT-05 Unstructured remittance information
+ ];
+
+ return {
+ type: "ok",
+ qrContent: lines.join("\n"),
+ };
+}
+
+/**
+ * Specification of a QR code that includes payment information.
+ */
+export interface QrCodeSpec {
+ /**
+ * Type of the QR code.
+ *
+ * Depending on the type, different visual styles
+ * might be applied.
+ */
+ type: string;
+
+ /**
+ * Content of the QR code that should be rendered.
+ */
+ qrContent: string;
+}
+
+/**
+ * Get applicable QR code specifications for the given payto URI.
+ */
+export function getQrCodesForPayto(paytoUri: string): QrCodeSpec[] {
+ const res: QrCodeSpec[] = [];
+ {
+ const qr = encodePaytoAsEpcQr(paytoUri);
+ if (qr.type == "ok") {
+ res.push({
+ type: "epc-qr",
+ qrContent: qr.qrContent,
+ });
+ }
+ }
+ {
+ const qr = encodePaytoAsSwissQrBill(paytoUri);
+ if (qr.type == "ok") {
+ res.push({
+ type: "spc",
+ qrContent: qr.qrContent,
+ });
+ }
+ }
+ return res;
+}
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 950161b10..ca57e22e2 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -30,14 +30,13 @@ import { hmacSha256, hmacSha512 } from "./kdf.js";
import { Logger } from "./logging.js";
import * as nacl from "./nacl-fast.js";
import { secretbox } from "./nacl-fast.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+import { CoinPublicKeyString, HashCodeString } from "./types-taler-common.js";
import {
CoinEnvelope,
- CoinPublicKeyString,
DenomKeyType,
DenominationPubKey,
- HashCodeString,
-} from "./taler-types.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
+} from "./types-taler-exchange.js";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `taler.${FlavorT}`;
@@ -987,6 +986,7 @@ export enum TalerSignaturePurpose {
MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
+ WALLET_ACCOUNT_SETUP = 1205,
WALLET_COIN_RECOUP_REFRESH = 1206,
WALLET_AGE_ATTESTATION = 1207,
WALLET_PURSE_CREATE = 1210,
@@ -998,9 +998,10 @@ export enum TalerSignaturePurpose {
WALLET_COIN_HISTORY = 1209,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
- TALER_SIGNATURE_AML_DECISION = 1350,
- TALER_SIGNATURE_AML_QUERY = 1351,
- TALER_SIGNATURE_MASTER_AML_KEY = 1017,
+ AML_DECISION = 1350,
+ AML_QUERY = 1351,
+ MASTER_AML_KEY = 1017,
+ KYC_AUTH = 1360,
ANASTASIS_POLICY_UPLOAD = 1400,
ANASTASIS_POLICY_DOWNLOAD = 1401,
SYNC_BACKUP_UPLOAD = 1450,
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index c463d94a0..cc1c9f78b 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -49,6 +49,14 @@ export enum TalerErrorCode {
/**
+ * The client does not support the protocol version advertised by the server.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION = 3,
+
+
+ /**
* The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
@@ -177,6 +185,14 @@ export enum TalerErrorCode {
/**
+ * A segment in the path of the URL provided by the client is malformed. Check that you are using the correct encoding for the URL.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_PATH_SEGMENT_MALFORMED = 29,
+
+
+ /**
* The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
@@ -673,6 +689,38 @@ export enum TalerErrorCode {
/**
+ * The KYC operation failed. This could be because the KYC provider rejected the KYC data provided, or because the user aborted the KYC process.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FAILED = 1038,
+
+
+ /**
+ * A fallback measure for a KYC operation failed. This is a bug. Users should contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FALLBACK_FAILED = 1039,
+
+
+ /**
+ * The specified fallback measure for a KYC operation is unknown. This is a bug. Users should contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_FALLBACK_UNKNOWN = 1040,
+
+
+ /**
+ * The exchange is not aware of the bank account (payto URI or hash thereof) specified in the request and thus cannot perform the requested operation. The client should check that the select account is correct.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_BANK_ACCOUNT_UNKNOWN = 1041,
+
+
+ /**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -1905,6 +1953,54 @@ export enum TalerErrorCode {
/**
+ * The KYC info access token is not recognized. Hence the request was denied.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED = 1919,
+
+
+ /**
+ * The exchange got stuck in a long series of (likely recursive) KYC rules without user-inputs that did not result in a timely conclusion. This is a configuration failure. Please contact the administrator.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_RECURSIVE_RULE_DETECTED = 1920,
+
+
+ /**
+ * The submitted KYC data lacks an attribute that is required by the KYC form. Please submit the complete form.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_FORM_INCOMPLETE = 1921,
+
+
+ /**
+ * The request requires an AML program which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_AML_PROGRAM_GONE = 1922,
+
+
+ /**
+ * The given check is not of type 'form' and thus using this handler for form submission is incorrect.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_NOT_A_FORM = 1923,
+
+
+ /**
+ * The request requires a check which is no longer configured at the exchange. Contact the exchange operator to address the configuration issue.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_CHECK_GONE = 1924,
+
+
+ /**
* The signature affirming the wallet's KYC request was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
@@ -2033,6 +2129,78 @@ export enum TalerErrorCode {
/**
+ * The form has been previously uploaded, and may only be filed once. The user should be redirected to their main KYC page and see if any other steps need to be taken.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_FORM_ALREADY_UPLOADED = 1941,
+
+
+ /**
+ * The internal state of the exchange specifying KYC measures is malformed. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_MEASURES_MALFORMED = 1942,
+
+
+ /**
+ * The specified index does not refer to a valid KYC measure. Please check the URL.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_MEASURE_INDEX_INVALID = 1943,
+
+
+ /**
+ * The operation is not supported by the selected KYC logic. This is either caused by a configuration change or some invalid use of the API. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_INVALID_LOGIC_TO_CHECK = 1944,
+
+
+ /**
+ * The AML program failed. This is either caused by a configuration change or a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_PROGRAM_FAILURE = 1945,
+
+
+ /**
+ * The AML program returned a malformed result. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_AML_PROGRAM_MALFORMED_RESULT = 1946,
+
+
+ /**
+ * The response from the KYC provider lacked required attributes. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_REPLY = 1947,
+
+
+ /**
+ * The context of the KYC check lacked required fields. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_PROVIDER_INCOMPLETE_CONTEXT = 1948,
+
+
+ /**
+ * The logic plugin had a bug in its AML processing. This is a bug. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG = 1949,
+
+
+ /**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2289,6 +2457,14 @@ export enum TalerErrorCode {
/**
+ * The exchange specified in the operation is not trusted by this exchange. The client should limit its operation to exchanges enabled by the merchant, or ask the merchant to enable additional exchanges in the configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_UNTRUSTED = 2025,
+
+
+ /**
* The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
* (A value of 0 indicates that the error is generated client-side).
@@ -2585,6 +2761,22 @@ export enum TalerErrorCode {
/**
+ * Invalid token because it was already used, is expired or not yet valid.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID = 2183,
+
+
+ /**
+ * The payment violates a transaction limit configured at the given exchange. The wallet has a bug in that it failed to check exchange limits during coin selection. Please report the bug to your wallet developer.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION = 2184,
+
+
+ /**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
@@ -2897,6 +3089,22 @@ export enum TalerErrorCode {
/**
+ * The refund amount would violate a refund transaction limit configured at the given exchange. Please find another way to refund the customer, and inquire with your legislator why they make strange banking regulations.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION = 2512,
+
+
+ /**
+ * The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS = 2513,
+
+
+ /**
* The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
@@ -2945,6 +3153,14 @@ export enum TalerErrorCode {
/**
+ * A token family referenced in this order is either expired or not valid yet.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_NOT_VALID = 2534,
+
+
+ /**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
@@ -4113,6 +4329,62 @@ export enum TalerErrorCode {
/**
+ * A wallet-core request failed because the user needs to first accept the exchange's terms of service.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_TOS_NOT_ACCEPTED = 7037,
+
+
+ /**
+ * An exchange entry could not be updated, as the exchange's new details conflict with the new details.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT = 7038,
+
+
+ /**
+ * The wallet's information about the exchange is outdated.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_ENTRY_OUTDATED = 7039,
+
+
+ /**
+ * The merchant needs to do KYC first, the payment could not be completed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PAY_MERCHANT_KYC_MISSING = 7040,
+
+
+ /**
+ * A peer-pull-debit transaction was aborted because the exchange reported the purse as gone.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PULL_DEBIT_PURSE_GONE = 7041,
+
+
+ /**
+ * A transaction was aborted on explicit request by the user.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TRANSACTION_ABORTED_BY_USER = 7042,
+
+
+ /**
+ * A transaction was abandoned on explicit request by the user.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TRANSACTION_ABANDONED_BY_USER = 7043,
+
+
+ /**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/taler-signatures.ts b/packages/taler-util/src/taler-signatures.ts
new file mode 100644
index 000000000..f529a456b
--- /dev/null
+++ b/packages/taler-util/src/taler-signatures.ts
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 GNUnet e.V.
+
+ 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 { canonicalJson } from "./index.js";
+import {
+ bufferForUint64,
+ buildSigPS,
+ decodeCrock,
+ eddsaSign,
+ hash,
+ stringToBytes,
+ TalerSignaturePurpose,
+ timestampRoundedToBuffer,
+} from "./taler-crypto.js";
+import { AmlDecisionRequestWithoutSignature } from "./types-taler-exchange.js";
+
+/**
+ * Implementation of Taler protocol signatures.
+ *
+ * In this file, we have implementations of signatures that are not used in the wallet,
+ * but in other places (tests, SPAs, ...).
+ */
+
+/**
+ * Signature for the POST /aml/$OFFICER_PUB/decisions endpoint.
+ */
+export function signAmlDecision(
+ priv: Uint8Array,
+ decision: AmlDecisionRequestWithoutSignature,
+): Uint8Array {
+ const builder = buildSigPS(TalerSignaturePurpose.AML_DECISION);
+
+ const flags: number = decision.keep_investigating ? 1 : 0;
+
+ builder.put(timestampRoundedToBuffer(decision.decision_time));
+ builder.put(decodeCrock(decision.h_payto));
+ builder.put(hash(stringToBytes(decision.justification)));
+ builder.put(hash(stringToBytes(canonicalJson(decision.properties) + "\0")));
+ builder.put(hash(stringToBytes(canonicalJson(decision.new_rules) + "\0")));
+ if (decision.new_measures != null) {
+ builder.put(hash(stringToBytes(decision.new_measures)));
+ } else {
+ builder.put(new Uint8Array(64));
+ }
+ builder.put(bufferForUint64(flags));
+
+ const sigBlob = builder.build();
+
+ return eddsaSign(sigBlob, priv);
+}
+
+export function signAmlQuery(key: Uint8Array): Uint8Array {
+ const sigBlob = buildSigPS(TalerSignaturePurpose.AML_QUERY).build();
+
+ return eddsaSign(sigBlob, key);
+}
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
deleted file mode 100644
index 66f98ea9a..000000000
--- a/packages/taler-util/src/taler-types.ts
+++ /dev/null
@@ -1,2421 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- 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/>
- */
-
-/**
- * Type and schema definitions and helpers for the core GNU Taler protocol.
- *
- * Even though the rest of the wallet uses camelCase for fields, use snake_case
- * here, since that's the convention for the Taler JSON+HTTP API.
- */
-
-/**
- * Imports.
- */
-
-import { Amounts, codecForAmountString } from "./amounts.js";
-import {
- Codec,
- buildCodecForObject,
- buildCodecForUnion,
- codecForAny,
- codecForBoolean,
- codecForConstString,
- codecForList,
- codecForMap,
- codecForNumber,
- codecForString,
- codecForStringURL,
- codecOptional,
-} from "./codec.js";
-import { strcmp } from "./helpers.js";
-import {
- CurrencySpecification,
- codecForCurrencySpecificiation,
- codecForEither,
- codecForProduct,
-} from "./index.js";
-import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
-import {
- TalerProtocolDuration,
- TalerProtocolTimestamp,
- codecForDuration,
- codecForTimestamp,
-} from "./time.js";
-
-/**
- * Denomination as found in the /keys response from the exchange.
- */
-export class ExchangeDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: string;
-
- /**
- * Public signing key of the denomination.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: string;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: string;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: string;
-
- /**
- * Fee for refunding.
- */
- fee_refund: string;
-
- /**
- * Start date from which withdraw is allowed.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * End date for withdrawing.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Expiration date after which the exchange can forget about
- * the currency.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Date after which the coins of this denomination can't be
- * deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature over the denomination information by the exchange's master
- * signing key.
- */
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class AuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class ExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: AuditorDenomSig[];
-}
-
-export type ExchangeWithdrawValue =
- | ExchangeRsaWithdrawValue
- | ExchangeCsWithdrawValue;
-
-export interface ExchangeRsaWithdrawValue {
- cipher: "RSA";
-}
-
-export interface ExchangeCsWithdrawValue {
- cipher: "CS";
-
- /**
- * CSR R0 value
- */
- r_pub_0: string;
-
- /**
- * CSR R1 value
- */
- r_pub_1: string;
-}
-
-export interface RecoupRequest {
- /**
- * Hashed denomination public key of the coin we want to get
- * paid back.
- */
- denom_pub_hash: string;
-
- /**
- * Signature over the coin public key by the denomination.
- *
- * The string variant is for the legacy exchange protocol.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Blinding key that was used during withdraw,
- * used to prove that we were actually withdrawing the coin.
- */
- coin_blind_key_secret: string;
-
- /**
- * Signature of TALER_RecoupRequestPS created with the coin's private key.
- */
- coin_sig: string;
-
- ewv: ExchangeWithdrawValue;
-}
-
-export interface RecoupRefreshRequest {
- /**
- * Hashed enomination public key of the coin we want to get
- * paid back.
- */
- denom_pub_hash: string;
-
- /**
- * Signature over the coin public key by the denomination.
- *
- * The string variant is for the legacy exchange protocol.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Coin's blinding factor.
- */
- coin_blind_key_secret: string;
-
- /**
- * Signature of TALER_RecoupRefreshRequestPS created with
- * the coin's private key.
- */
- coin_sig: string;
-
- ewv: ExchangeWithdrawValue;
-}
-
-/**
- * Response that we get from the exchange for a payback request.
- */
-export interface RecoupConfirmation {
- /**
- * Public key of the reserve that will receive the payback.
- */
- reserve_pub?: string;
-
- /**
- * Public key of the old coin that will receive the recoup,
- * provided if refreshed was true.
- */
- old_coin_pub?: string;
-}
-
-export type UnblindedSignature = RsaUnblindedSignature;
-
-export interface RsaUnblindedSignature {
- cipher: DenomKeyType.Rsa;
- rsa_signature: string;
-}
-
-/**
- * Deposit permission for a single coin.
- */
-export interface CoinDepositPermission {
- /**
- * Signature by the coin.
- */
- coin_sig: string;
-
- /**
- * Public key of the coin being spend.
- */
- coin_pub: string;
-
- /**
- * Signature made by the denomination public key.
- *
- * The string variant is for legacy protocol support.
- */
-
- ub_sig: UnblindedSignature;
-
- /**
- * The denomination public key associated with this coin.
- */
- h_denom: string;
-
- /**
- * The amount that is subtracted from this coin with this payment.
- */
- contribution: string;
-
- /**
- * URL of the exchange this coin was withdrawn from.
- */
- exchange_url: string;
-
- minimum_age_sig?: EddsaSignatureString;
-
- age_commitment?: Edx25519PublicKeyEnc[];
-
- h_age_commitment?: string;
-}
-
-/**
- * Information about an exchange as stored inside a
- * merchant's contract terms.
- */
-export interface ExchangeHandle {
- // The exchange's base URL.
- url: string;
-
- // Master public key of the exchange.
- master_pub: EddsaPublicKeyString;
-}
-
-export interface AuditorHandle {
- /**
- * Official name of the auditor.
- */
- name: string;
-
- /**
- * Master public signing key of the auditor.
- */
- auditor_pub: EddsaPublicKeyString;
-
- /**
- * Base URL of the auditor.
- */
- url: string;
-}
-
-// Delivery location, loosely modeled as a subset of
-// ISO20022's PostalAddress25.
-export 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[];
-}
-
-export interface MerchantInfo {
- // The merchant's legal name of business.
- name: string;
-
- // Label for a location with the business address of the merchant.
- email?: string;
-
- // Label for a location with the business address of the merchant.
- website?: string;
-
- // An optional base64-encoded product image.
- logo?: ImageDataUrl;
-
- // Label for a location with the business address of the merchant.
- address?: Location;
-
- // 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;
-}
-
-export interface Tax {
- // the name of the tax
- name: string;
-
- // amount paid in tax
- tax: AmountString;
-}
-
-export 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?: InternationalizedString;
-
- // 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?: AmountString;
-
- // 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;
-}
-
-export interface InternationalizedString {
- [lang_tag: string]: string;
-}
-
-/**
- * Contract terms from a merchant.
- * FIXME: Add type field!
- */
-export interface MerchantContractTerms {
- // The hash of the merchant instance's wire details.
- h_wire: 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?: TalerProtocolDuration;
-
- // 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;
-
- // Human-readable description of the whole purchase.
- summary: string;
-
- // Map from IETF BCP 47 language tags to localized summaries.
- summary_i18n?: InternationalizedString;
-
- // 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: string;
-
- // Nonce generated by the wallet and echoed by the merchant
- // in this field when the proposal is generated.
- nonce: string;
-
- // After this deadline, the merchant won't accept payments for the contract.
- pay_deadline: TalerProtocolTimestamp;
-
- // More info about the merchant, see below.
- merchant: MerchantInfo;
-
- // 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: string;
-
- // Time indicating when the order should be delivered.
- // May be overwritten by individual products.
- delivery_date?: TalerProtocolTimestamp;
-
- // Delivery location for (all!) products.
- delivery_location?: Location;
-
- // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
- exchanges: ExchangeHandle[];
-
- // List of products that are part of the purchase (see Product).
- products?: Product[];
-
- // After this deadline has passed, no refunds will be accepted.
- refund_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;
-
- // Time when this contract was generated.
- timestamp: TalerProtocolTimestamp;
-
- // Base URL of the (public!) merchant backend API.
- // Must be an absolute URL that ends with a slash.
- merchant_base_url: string;
-
- // URL that will show that the order was successful after
- // it has been paid for. Optional, but either fulfillment_url
- // or fulfillment_message must be specified in every
- // contract terms.
- //
- // If a non-unique fulfillment URL is used, a customer can only
- // buy the order once and will be redirected to a previous purchase
- // when trying to buy an order with the same fulfillment URL a second
- // time. This is useful for digital goods that a customer only needs
- // to buy once but should be able to repeatedly download.
- //
- // For orders where the customer is expected to be able to make
- // repeated purchases (for equivalent goods), the fulfillment URL
- // should be made unique for every order. The easiest way to do
- // this is to include a unique order ID in the fulfillment URL.
- //
- // When POSTing to the merchant, the placeholder text "${ORDER_ID}"
- // is be replaced with the actual order ID (useful if the
- // order ID is generated server-side and needs to be
- // in the URL). Note that this placeholder can only be used once.
- // Front-ends may use other means to generate a unique fulfillment URL.
- fulfillment_url?: string;
-
- // URL where the same contract could be ordered again (if
- // available). Returned also at the public order endpoint
- // for people other than the actual buyer (hence public,
- // in case order IDs are guessable).
- public_reorder_url?: string;
-
- // Message shown to the customer after paying for the order.
- // Either fulfillment_url or fulfillment_message must be specified.
- fulfillment_message?: string;
-
- // Map from IETF BCP 47 language tags to localized fulfillment
- // messages.
- fulfillment_message_i18n?: InternationalizedString;
-
- // Maximum total deposit fee accepted by the merchant for this contract.
- // Overrides defaults of the merchant instance.
- max_fee: string;
-
- // 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.
- // Must really be an Object (not a string, integer, float or array).
- extra?: any;
-
- // Minimum age the buyer must have (in years). Default is 0.
- // This value is at least as large as the maximum over all
- // minimum age requirements of the products in this contract.
- // It might also be set independent of any product, due to
- // legal requirements.
- minimum_age?: Integer;
-}
-
-/**
- * Refund permission in the format that the merchant gives it to us.
- */
-export interface MerchantAbortPayRefundDetails {
- /**
- * Amount to be refunded.
- */
- refund_amount: string;
-
- /**
- * Fee for the refund.
- */
- refund_fee: string;
-
- /**
- * Public key of the coin being refunded.
- */
- coin_pub: string;
-
- /**
- * Refund transaction ID between merchant and exchange.
- */
- rtransaction_id: number;
-
- /**
- * Exchange's key used for the signature.
- */
- exchange_pub?: string;
-
- /**
- * Exchange's signature to confirm the refund.
- */
- exchange_sig?: string;
-
- /**
- * Error replay from the exchange (if any).
- */
- exchange_reply?: any;
-
- /**
- * Error code from the exchange (if any).
- */
- exchange_code?: number;
-
- /**
- * HTTP status code of the exchange's response
- * to the merchant's refund request.
- */
- exchange_http_status: number;
-}
-
-/**
- * Planchet detail sent to the merchant.
- */
-export interface TipPlanchetDetail {
- /**
- * Hashed denomination public key.
- */
- denom_pub_hash: string;
-
- /**
- * Coin's blinded public key.
- */
- coin_ev: CoinEnvelope;
-}
-
-/**
- * Request sent to the merchant to pick up a tip.
- */
-export interface TipPickupRequest {
- /**
- * Identifier of the tip.
- */
- tip_id: string;
-
- /**
- * List of planchets the wallet wants to use for the tip.
- */
- planchets: TipPlanchetDetail[];
-}
-
-/**
- * Reserve signature, defined as separate class to facilitate
- * schema validation.
- */
-export interface MerchantBlindSigWrapperV1 {
- /**
- * Reserve signature.
- */
- blind_sig: string;
-}
-
-/**
- * Response of the merchant
- * to the TipPickupRequest.
- */
-export interface MerchantTipResponseV1 {
- /**
- * The order of the signatures matches the planchets list.
- */
- blind_sigs: MerchantBlindSigWrapperV1[];
-}
-
-export interface MerchantBlindSigWrapperV2 {
- blind_sig: BlindedDenominationSignature;
-}
-
-/**
- * Response of the merchant
- * to the TipPickupRequest.
- */
-export interface MerchantTipResponseV2 {
- /**
- * The order of the signatures matches the planchets list.
- */
- blind_sigs: MerchantBlindSigWrapperV2[];
-}
-
-/**
- * Element of the payback list that the
- * exchange gives us in /keys.
- */
-export class Recoup {
- /**
- * The hash of the denomination public key for which the payback is offered.
- */
- h_denom_pub: string;
-}
-
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class ExchangeSignKeyJson {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: EddsaPublicKeyString;
- master_sig: EddsaSignatureString;
-}
-
-/**
- * Structure that the exchange gives us in /keys.
- */
-export class ExchangeKeysJson {
- /**
- * Canonical, public base URL of the exchange.
- */
- base_url: string;
-
- currency: string;
-
- /**
- * The exchange's master public key.
- */
- master_public_key: string;
-
- /**
- * The list of auditors (partially) auditing the exchange.
- */
- auditors: ExchangeAuditor[];
-
- /**
- * Timestamp when this response was issued.
- */
- list_issue_date: TalerProtocolTimestamp;
-
- /**
- * List of revoked denominations.
- */
- recoup?: Recoup[];
-
- /**
- * Short-lived signing keys used to sign online
- * responses.
- */
- signkeys: ExchangeSignKeyJson[];
-
- /**
- * Protocol version.
- */
- version: string;
-
- reserve_closing_delay: TalerProtocolDuration;
-
- global_fees: GlobalFees[];
-
- accounts: ExchangeWireAccount[];
-
- wire_fees: { [methodName: string]: WireFeesJson[] };
-
- denominations: DenomGroup[];
-}
-
-export type DenomGroup =
- | DenomGroupRsa
- | DenomGroupCs
- | DenomGroupRsaAgeRestricted
- | DenomGroupCsAgeRestricted;
-
-export interface DenomGroupCommon {
- // How much are coins of this denomination worth?
- value: AmountString;
-
- // Fee charged by the exchange for withdrawing a coin of this denomination.
- fee_withdraw: AmountString;
-
- // Fee charged by the exchange for depositing a coin of this denomination.
- fee_deposit: AmountString;
-
- // Fee charged by the exchange for refreshing a coin of this denomination.
- fee_refresh: AmountString;
-
- // Fee charged by the exchange for refunding a coin of this denomination.
- fee_refund: AmountString;
-
- // XOR of all the SHA-512 hash values of the denominations' public keys
- // in this group. Note that for hashing, the binary format of the
- // public keys is used, and not their base32 encoding.
- hash: HashCodeString;
-}
-
-export interface DenomCommon {
- // Signature of TALER_DenominationKeyValidityPS.
- master_sig: EddsaSignatureString;
-
- // When does the denomination key become valid?
- stamp_start: TalerProtocolTimestamp;
-
- // When is it no longer possible to deposit coins
- // of this denomination?
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- // Timestamp indicating by when legal disputes relating to these coins must
- // be settled, as the exchange will afterwards destroy its evidence relating to
- // transactions involving this coin.
- stamp_expire_legal: TalerProtocolTimestamp;
-
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- // Set to 'true' if the exchange somehow "lost"
- // the private key. The denomination was not
- // necessarily revoked, but still cannot be used
- // to withdraw coins at this time (theoretically,
- // the private key could be recovered in the
- // future; coins signed with the private key
- // remain valid).
- lost?: boolean;
-}
-
-export type RsaPublicKeySring = string;
-export type AgeMask = number;
-export type ImageDataUrl = string;
-
-/**
- * 32-byte value representing a point on Curve25519.
- */
-export type Cs25519Point = string;
-
-export interface DenomGroupRsa extends DenomGroupCommon {
- cipher: "RSA";
-
- denoms: ({
- rsa_pub: RsaPublicKeySring;
- } & DenomCommon)[];
-}
-
-export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
- cipher: "RSA+age_restricted";
- age_mask: AgeMask;
-
- denoms: ({
- rsa_pub: RsaPublicKeySring;
- } & DenomCommon)[];
-}
-
-export interface DenomGroupCs extends DenomGroupCommon {
- cipher: "CS";
- age_mask: AgeMask;
-
- denoms: ({
- cs_pub: Cs25519Point;
- } & DenomCommon)[];
-}
-
-export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
- cipher: "CS+age_restricted";
- age_mask: AgeMask;
-
- denoms: ({
- cs_pub: Cs25519Point;
- } & DenomCommon)[];
-}
-
-export interface GlobalFees {
- // What date (inclusive) does these fees go into effect?
- start_date: TalerProtocolTimestamp;
-
- // What date (exclusive) does this fees stop going into effect?
- end_date: TalerProtocolTimestamp;
-
- // Account history fee, charged when a user wants to
- // obtain a reserve/account history.
- history_fee: AmountString;
-
- // Annual fee charged for having an open account at the
- // exchange. Charged to the account. If the account
- // balance is insufficient to cover this fee, the account
- // is automatically deleted/closed. (Note that the exchange
- // will keep the account history around for longer for
- // regulatory reasons.)
- account_fee: AmountString;
-
- // Purse fee, charged only if a purse is abandoned
- // and was not covered by the account limit.
- purse_fee: AmountString;
-
- // How long will the exchange preserve the account history?
- // After an account was deleted/closed, the exchange will
- // retain the account history for legal reasons until this time.
- history_expiration: TalerProtocolDuration;
-
- // Non-negative number of concurrent purses that any
- // account holder is allowed to create without having
- // to pay the purse_fee.
- purse_account_limit: number;
-
- // How long does an exchange keep a purse around after a purse
- // has expired (or been successfully merged)? A 'GET' request
- // for a purse will succeed until the purse expiration time
- // plus this value.
- purse_timeout: TalerProtocolDuration;
-
- // Signature of TALER_GlobalFeesPS.
- master_sig: string;
-}
-/**
- * Wire fees as announced by the exchange.
- */
-export class WireFeesJson {
- /**
- * Cost of a wire transfer.
- */
- wire_fee: string;
-
- /**
- * Cost of clising a reserve.
- */
- closing_fee: string;
-
- /**
- * Signature made with the exchange's master key.
- */
- sig: string;
-
- /**
- * Date from which the fee applies.
- */
- start_date: TalerProtocolTimestamp;
-
- /**
- * Data after which the fee doesn't apply anymore.
- */
- end_date: TalerProtocolTimestamp;
-}
-
-/**
- * Proposal returned from the contract URL.
- */
-export class Proposal {
- /**
- * Contract terms for the propoal.
- * Raw, un-decoded JSON object.
- */
- contract_terms: any;
-
- /**
- * Signature over contract, made by the merchant. The public key used for signing
- * must be contract_terms.merchant_pub.
- */
- sig: string;
-}
-
-/**
- * Response from the internal merchant API.
- */
-export class CheckPaymentResponse {
- order_status: string;
- refunded: boolean | undefined;
- refunded_amount: string | undefined;
- contract_terms: any | undefined;
- taler_pay_uri: string | undefined;
- contract_url: string | undefined;
-}
-
-/**
- * Response from the bank.
- */
-export class WithdrawOperationStatusResponse {
- status: "selected" | "aborted" | "confirmed" | "pending";
-
- selection_done: boolean;
-
- transfer_done: boolean;
-
- aborted: boolean;
-
- amount: string | undefined;
-
- sender_wire?: string;
-
- suggested_exchange?: string;
-
- confirm_transfer_url?: string;
-
- wire_types: string[];
-}
-
-/**
- * Response from the merchant.
- */
-export class RewardPickupGetResponse {
- reward_amount: string;
-
- exchange_url: string;
-
- next_url?: string;
-
- expiration: TalerProtocolTimestamp;
-}
-
-export enum DenomKeyType {
- Rsa = "RSA",
- ClauseSchnorr = "CS",
-}
-
-export namespace DenomKeyType {
- export function toIntTag(t: DenomKeyType): number {
- switch (t) {
- case DenomKeyType.Rsa:
- return 1;
- case DenomKeyType.ClauseSchnorr:
- return 2;
- }
- }
-}
-
-export interface RsaBlindedDenominationSignature {
- cipher: DenomKeyType.Rsa;
- blinded_rsa_signature: string;
-}
-
-export interface CSBlindedDenominationSignature {
- cipher: DenomKeyType.ClauseSchnorr;
-}
-
-export type BlindedDenominationSignature =
- | RsaBlindedDenominationSignature
- | CSBlindedDenominationSignature;
-
-export const codecForRsaBlindedDenominationSignature = () =>
- buildCodecForObject<RsaBlindedDenominationSignature>()
- .property("cipher", codecForConstString(DenomKeyType.Rsa))
- .property("blinded_rsa_signature", codecForString())
- .build("RsaBlindedDenominationSignature");
-
-export const codecForBlindedDenominationSignature = () =>
- buildCodecForUnion<BlindedDenominationSignature>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
- .build("BlindedDenominationSignature");
-
-export class ExchangeWithdrawResponse {
- ev_sig: BlindedDenominationSignature;
-}
-
-export class ExchangeWithdrawBatchResponse {
- ev_sigs: ExchangeWithdrawResponse[];
-}
-
-export interface MerchantPayResponse {
- sig: string;
- pos_confirmation?: string;
-}
-
-export interface ExchangeMeltRequest {
- coin_pub: CoinPublicKeyString;
- confirm_sig: EddsaSignatureString;
- denom_pub_hash: HashCodeString;
- denom_sig: UnblindedSignature;
- rc: string;
- value_with_fee: AmountString;
- age_commitment_hash?: HashCodeString;
-}
-
-export interface ExchangeMeltResponse {
- /**
- * Which of the kappa indices does the client not have to reveal.
- */
- noreveal_index: number;
-
- /**
- * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
- * affirms the successful melt and confirming the noreveal_index
- */
- exchange_sig: EddsaSignatureString;
-
- /*
- * public EdDSA key of the exchange that was used to generate the signature.
- * Should match one of the exchange's signing keys from /keys. Again given
- * explicitly as the client might otherwise be confused by clock skew as to
- * which signing key was used.
- */
- exchange_pub: EddsaPublicKeyString;
-
- /*
- * Base URL to use for operations on the refresh context
- * (so the reveal operation). If not given,
- * the base URL is the same as the one used for this request.
- * Can be used if the base URL for /refreshes/ differs from that
- * for /coins/, i.e. for load balancing. Clients SHOULD
- * respect the refresh_base_url if provided. Any HTTP server
- * belonging to an exchange MUST generate a 307 or 308 redirection
- * to the correct base URL should a client uses the wrong base
- * URL, or if the base URL has changed since the melt.
- *
- * When melting the same coin twice (technically allowed
- * as the response might have been lost on the network),
- * the exchange may return different values for the refresh_base_url.
- */
- refresh_base_url?: string;
-}
-
-export interface ExchangeRevealItem {
- ev_sig: BlindedDenominationSignature;
-}
-
-export interface ExchangeRevealResponse {
- // List of the exchange's blinded RSA signatures on the new coins.
- ev_sigs: ExchangeRevealItem[];
-}
-
-interface MerchantOrderStatusPaid {
- // Was the payment refunded (even partially, via refund or abort)?
- refunded: boolean;
-
- // Is any amount of the refund still waiting to be picked up (even partially)?
- refund_pending: boolean;
-
- // Amount that was refunded in total.
- refund_amount: AmountString;
-
- // Amount that already taken by the wallet.
- refund_taken: AmountString;
-}
-
-interface MerchantOrderRefundResponse {
- /**
- * Amount that was refunded in total.
- */
- refund_amount: AmountString;
-
- /**
- * Successful refunds for this payment, empty array for none.
- */
- refunds: MerchantCoinRefundStatus[];
-
- /**
- * Public key of the merchant.
- */
- merchant_pub: EddsaPublicKeyString;
-}
-
-export type MerchantCoinRefundStatus =
- | MerchantCoinRefundSuccessStatus
- | MerchantCoinRefundFailureStatus;
-
-export interface MerchantCoinRefundSuccessStatus {
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // the EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund
- exchange_sig: EddsaSignatureString;
-
- // public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKeyString;
-
- // Refund transaction ID.
- rtransaction_id: number;
-
- // public key of a coin that was refunded
- coin_pub: EddsaPublicKeyString;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- execution_time: TalerProtocolTimestamp;
-}
-
-export interface MerchantCoinRefundFailureStatus {
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: number;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: number;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: any;
-
- // Refund transaction ID.
- rtransaction_id: number;
-
- // public key of a coin that was refunded
- coin_pub: EddsaPublicKeyString;
-
- // Amount that was refunded, including refund fee charged by the exchange
- // to the customer.
- refund_amount: AmountString;
-
- execution_time: TalerProtocolTimestamp;
-}
-
-export interface MerchantOrderStatusUnpaid {
- /**
- * 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;
-}
-
-/**
- * Response body for the following endpoint:
- *
- * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
- */
-export interface BankWithdrawalOperationPostResponse {
- // Current status of the operation
- // pending: the operation is pending parameters selection (exchange and reserve public key)
- // selected: the operations has been selected and is pending confirmation
- // aborted: the operation has been aborted
- // confirmed: the transfer has been confirmed and registered by the bank
- status: "selected" | "aborted" | "confirmed" | "pending";
-
- // URL that the user needs to navigate to in order to
- // complete some final confirmation (e.g. 2FA).
- //
- // Only applicable when status is selected or pending.
- // It may contain withdrawal operation id
- confirm_transfer_url?: string;
-
- // Deprecated field use status instead
- // The transfer has been confirmed and registered by the bank.
- // Does not guarantee that the funds have arrived at the exchange already.
- transfer_done: boolean;
-}
-
-export const codeForBankWithdrawalOperationPostResponse =
- (): Codec<BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<BankWithdrawalOperationPostResponse>()
- .property(
- "status",
- codecForEither(
- codecForConstString("selected"),
- codecForConstString("confirmed"),
- codecForConstString("aborted"),
- codecForConstString("pending"),
- ),
- )
- .property("confirm_transfer_url", codecOptional(codecForString()))
- .property("transfer_done", codecForBoolean())
- .build("BankWithdrawalOperationPostResponse");
-
-export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
-
-export interface RsaDenominationPubKey {
- readonly cipher: DenomKeyType.Rsa;
- readonly rsa_public_key: string;
- readonly age_mask: number;
-}
-
-export interface CsDenominationPubKey {
- readonly cipher: DenomKeyType.ClauseSchnorr;
- readonly age_mask: number;
- readonly cs_public_key: string;
-}
-
-export namespace DenominationPubKey {
- export function cmp(
- p1: DenominationPubKey,
- p2: DenominationPubKey,
- ): -1 | 0 | 1 {
- if (p1.cipher < p2.cipher) {
- return -1;
- } else if (p1.cipher > p2.cipher) {
- return +1;
- } else if (
- p1.cipher === DenomKeyType.Rsa &&
- p2.cipher === DenomKeyType.Rsa
- ) {
- if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
- return -1;
- } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
- return 1;
- }
- return strcmp(p1.rsa_public_key, p2.rsa_public_key);
- } else if (
- p1.cipher === DenomKeyType.ClauseSchnorr &&
- p2.cipher === DenomKeyType.ClauseSchnorr
- ) {
- if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
- return -1;
- } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
- return 1;
- }
- return strcmp(p1.cs_public_key, p2.cs_public_key);
- } else {
- throw Error("unsupported cipher");
- }
- }
-}
-
-export const codecForRsaDenominationPubKey = () =>
- buildCodecForObject<RsaDenominationPubKey>()
- .property("cipher", codecForConstString(DenomKeyType.Rsa))
- .property("rsa_public_key", codecForString())
- .property("age_mask", codecForNumber())
- .build("DenominationPubKey");
-
-export const codecForCsDenominationPubKey = () =>
- buildCodecForObject<CsDenominationPubKey>()
- .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
- .property("cs_public_key", codecForString())
- .property("age_mask", codecForNumber())
- .build("CsDenominationPubKey");
-
-export const codecForDenominationPubKey = () =>
- buildCodecForUnion<DenominationPubKey>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
- .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
- .build("DenominationPubKey");
-
-export type LitAmountString = `${string}:${number}`;
-
-declare const __amount_str: unique symbol;
-export type AmountString =
- | (string & { [__amount_str]: true })
- | LitAmountString;
-// export type AmountString = string;
-export type Base32String = string;
-export type EddsaSignatureString = string;
-export type EddsaPublicKeyString = string;
-export type EddsaPrivateKeyString = string;
-export type CoinPublicKeyString = string;
-
-export const codecForDenomination = (): Codec<ExchangeDenomination> =>
- buildCodecForObject<ExchangeDenomination>()
- .property("value", codecForString())
- .property("denom_pub", codecForDenominationPubKey())
- .property("fee_withdraw", codecForString())
- .property("fee_deposit", codecForString())
- .property("fee_refresh", codecForString())
- .property("fee_refund", codecForString())
- .property("stamp_start", codecForTimestamp)
- .property("stamp_expire_withdraw", codecForTimestamp)
- .property("stamp_expire_legal", codecForTimestamp)
- .property("stamp_expire_deposit", codecForTimestamp)
- .property("master_sig", codecForString())
- .build("Denomination");
-
-export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
- buildCodecForObject<AuditorDenomSig>()
- .property("denom_pub_h", codecForString())
- .property("auditor_sig", codecForString())
- .build("AuditorDenomSig");
-
-export const codecForAuditor = (): Codec<ExchangeAuditor> =>
- buildCodecForObject<ExchangeAuditor>()
- .property("auditor_pub", codecForString())
- .property("auditor_url", codecForString())
- .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
- .build("Auditor");
-
-export const codecForExchangeHandle = (): Codec<ExchangeHandle> =>
- buildCodecForObject<ExchangeHandle>()
- .property("master_pub", codecForString())
- .property("url", codecForString())
- .build("ExchangeHandle");
-
-export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
- buildCodecForObject<AuditorHandle>()
- .property("name", codecForString())
- .property("auditor_pub", codecForString())
- .property("url", codecForString())
- .build("AuditorHandle");
-
-export const codecForLocation = (): Codec<Location> =>
- buildCodecForObject<Location>()
- .property("country", codecOptional(codecForString()))
- .property("country_subdivision", codecOptional(codecForString()))
- .property("building_name", codecOptional(codecForString()))
- .property("building_number", codecOptional(codecForString()))
- .property("district", codecOptional(codecForString()))
- .property("street", codecOptional(codecForString()))
- .property("post_code", codecOptional(codecForString()))
- .property("town", codecOptional(codecForString()))
- .property("town_location", codecOptional(codecForString()))
- .property("address_lines", codecOptional(codecForList(codecForString())))
- .build("Location");
-
-export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
- buildCodecForObject<MerchantInfo>()
- .property("name", codecForString())
- .property("address", codecOptional(codecForLocation()))
- .property("jurisdiction", codecOptional(codecForLocation()))
- .build("MerchantInfo");
-
-export const codecForInternationalizedString =
- (): Codec<InternationalizedString> => codecForMap(codecForString());
-
-export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
- buildCodecForObject<MerchantContractTerms>()
- .property("order_id", codecForString())
- .property("fulfillment_url", codecOptional(codecForString()))
- .property("fulfillment_message", codecOptional(codecForString()))
- .property(
- "fulfillment_message_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("merchant_base_url", codecForString())
- .property("h_wire", codecForString())
- .property("auto_refund", codecOptional(codecForDuration))
- .property("wire_method", codecForString())
- .property("summary", codecForString())
- .property("summary_i18n", codecOptional(codecForInternationalizedString()))
- .property("nonce", codecForString())
- .property("amount", codecForAmountString())
- .property("pay_deadline", codecForTimestamp)
- .property("refund_deadline", codecForTimestamp)
- .property("wire_transfer_deadline", codecForTimestamp)
- .property("timestamp", codecForTimestamp)
- .property("delivery_location", codecOptional(codecForLocation()))
- .property("delivery_date", codecOptional(codecForTimestamp))
- .property("max_fee", codecForAmountString())
- .property("merchant", codecForMerchantInfo())
- .property("merchant_pub", codecForString())
- .property("exchanges", codecForList(codecForExchangeHandle()))
- .property("products", codecOptional(codecForList(codecForProduct())))
- .property("extra", codecForAny())
- .property("minimum_age", codecOptional(codecForNumber()))
- .build("MerchantContractTerms");
-
-export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
- buildCodecForObject<PeerContractTerms>()
- .property("summary", codecForString())
- .property("amount", codecForAmountString())
- .property("purse_expiration", codecForTimestamp)
- .build("PeerContractTerms");
-
-export const codecForMerchantRefundPermission =
- (): Codec<MerchantAbortPayRefundDetails> =>
- buildCodecForObject<MerchantAbortPayRefundDetails>()
- .property("refund_amount", codecForAmountString())
- .property("refund_fee", codecForAmountString())
- .property("coin_pub", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("exchange_http_status", codecForNumber())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("exchange_sig", codecOptional(codecForString()))
- .property("exchange_pub", codecOptional(codecForString()))
- .build("MerchantRefundPermission");
-
-export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
- buildCodecForObject<MerchantBlindSigWrapperV2>()
- .property("blind_sig", codecForBlindedDenominationSignature())
- .build("MerchantBlindSigWrapperV2");
-
-export const codecForMerchantTipResponseV2 = (): Codec<MerchantTipResponseV2> =>
- buildCodecForObject<MerchantTipResponseV2>()
- .property("blind_sigs", codecForList(codecForBlindSigWrapperV2()))
- .build("MerchantTipResponseV2");
-
-export const codecForRecoup = (): Codec<Recoup> =>
- buildCodecForObject<Recoup>()
- .property("h_denom_pub", codecForString())
- .build("Recoup");
-
-export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
- buildCodecForObject<ExchangeSignKeyJson>()
- .property("key", codecForString())
- .property("master_sig", codecForString())
- .property("stamp_end", codecForTimestamp)
- .property("stamp_start", codecForTimestamp)
- .property("stamp_expire", codecForTimestamp)
- .build("ExchangeSignKeyJson");
-
-export const codecForGlobalFees = (): Codec<GlobalFees> =>
- buildCodecForObject<GlobalFees>()
- .property("start_date", codecForTimestamp)
- .property("end_date", codecForTimestamp)
- .property("history_fee", codecForAmountString())
- .property("account_fee", codecForAmountString())
- .property("purse_fee", codecForAmountString())
- .property("history_expiration", codecForDuration)
- .property("purse_account_limit", codecForNumber())
- .property("purse_timeout", codecForDuration)
- .property("master_sig", codecForString())
- .build("GlobalFees");
-
-// FIXME: Validate properly!
-export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();
-
-export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
- buildCodecForObject<ExchangeKeysJson>()
- .property("base_url", codecForString())
- .property("currency", codecForString())
- .property("master_public_key", codecForString())
- .property("auditors", codecForList(codecForAuditor()))
- .property("list_issue_date", codecForTimestamp)
- .property("recoup", codecOptional(codecForList(codecForRecoup())))
- .property("signkeys", codecForList(codecForExchangeSigningKey()))
- .property("version", codecForString())
- .property("reserve_closing_delay", codecForDuration)
- .property("global_fees", codecForList(codecForGlobalFees()))
- .property("accounts", codecForList(codecForExchangeWireAccount()))
- .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
- .property("denominations", codecForList(codecForNgDenominations))
- .build("ExchangeKeysJson");
-
-export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
- buildCodecForObject<WireFeesJson>()
- .property("wire_fee", codecForString())
- .property("closing_fee", codecForString())
- .property("sig", codecForString())
- .property("start_date", codecForTimestamp)
- .property("end_date", codecForTimestamp)
- .build("WireFeesJson");
-
-export const codecForProposal = (): Codec<Proposal> =>
- buildCodecForObject<Proposal>()
- .property("contract_terms", codecForAny())
- .property("sig", codecForString())
- .build("Proposal");
-
-export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
- buildCodecForObject<CheckPaymentResponse>()
- .property("order_status", codecForString())
- .property("refunded", codecOptional(codecForBoolean()))
- .property("refunded_amount", codecOptional(codecForString()))
- .property("contract_terms", codecOptional(codecForAny()))
- .property("taler_pay_uri", codecOptional(codecForString()))
- .property("contract_url", codecOptional(codecForString()))
- .build("CheckPaymentResponse");
-
-export const codecForWithdrawOperationStatusResponse =
- (): Codec<WithdrawOperationStatusResponse> =>
- buildCodecForObject<WithdrawOperationStatusResponse>()
- .property(
- "status",
- codecForEither(
- codecForConstString("selected"),
- codecForConstString("confirmed"),
- codecForConstString("aborted"),
- codecForConstString("pending"),
- ),
- )
- .property("selection_done", codecForBoolean())
- .property("transfer_done", codecForBoolean())
- .property("aborted", codecForBoolean())
- .property("amount", codecOptional(codecForString()))
- .property("sender_wire", codecOptional(codecForString()))
- .property("suggested_exchange", codecOptional(codecForString()))
- .property("confirm_transfer_url", codecOptional(codecForString()))
- .property("wire_types", codecForList(codecForString()))
- .build("WithdrawOperationStatusResponse");
-
-export const codecForRewardPickupGetResponse =
- (): Codec<RewardPickupGetResponse> =>
- buildCodecForObject<RewardPickupGetResponse>()
- .property("reward_amount", codecForString())
- .property("exchange_url", codecForString())
- .property("next_url", codecOptional(codecForString()))
- .property("expiration", codecForTimestamp)
- .build("TipPickupGetResponse");
-
-export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
- buildCodecForObject<RecoupConfirmation>()
- .property("reserve_pub", codecOptional(codecForString()))
- .property("old_coin_pub", codecOptional(codecForString()))
- .build("RecoupConfirmation");
-
-export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
- buildCodecForObject<ExchangeWithdrawResponse>()
- .property("ev_sig", codecForBlindedDenominationSignature())
- .build("WithdrawResponse");
-
-export const codecForExchangeWithdrawBatchResponse =
- (): Codec<ExchangeWithdrawBatchResponse> =>
- buildCodecForObject<ExchangeWithdrawBatchResponse>()
- .property("ev_sigs", codecForList(codecForWithdrawResponse()))
- .build("WithdrawBatchResponse");
-
-export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
- buildCodecForObject<MerchantPayResponse>()
- .property("sig", codecForString())
- .property("pos_confirmation", codecOptional(codecForString()))
- .build("MerchantPayResponse");
-
-export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
- buildCodecForObject<ExchangeMeltResponse>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("noreveal_index", codecForNumber())
- .property("refresh_base_url", codecOptional(codecForString()))
- .build("ExchangeMeltResponse");
-
-export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
- buildCodecForObject<ExchangeRevealItem>()
- .property("ev_sig", codecForBlindedDenominationSignature())
- .build("ExchangeRevealItem");
-
-export const codecForExchangeRevealResponse =
- (): Codec<ExchangeRevealResponse> =>
- buildCodecForObject<ExchangeRevealResponse>()
- .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
- .build("ExchangeRevealResponse");
-
-export const codecForMerchantOrderStatusPaid =
- (): Codec<MerchantOrderStatusPaid> =>
- buildCodecForObject<MerchantOrderStatusPaid>()
- .property("refund_amount", codecForAmountString())
- .property("refund_taken", codecForAmountString())
- .property("refund_pending", codecForBoolean())
- .property("refunded", codecForBoolean())
- .build("MerchantOrderStatusPaid");
-
-export const codecForMerchantOrderStatusUnpaid =
- (): Codec<MerchantOrderStatusUnpaid> =>
- buildCodecForObject<MerchantOrderStatusUnpaid>()
- .property("taler_pay_uri", codecForString())
- .property("already_paid_order_id", codecOptional(codecForString()))
- .build("MerchantOrderStatusUnpaid");
-
-export interface AbortRequest {
- // hash of the order's contract terms (this is used to authenticate the
- // wallet/customer in case $ORDER_ID is guessable).
- h_contract: string;
-
- // List of coins the wallet would like to see refunds for.
- // (Should be limited to the coins for which the original
- // payment succeeded, as far as the wallet knows.)
- coins: AbortingCoin[];
-}
-
-export interface AbortingCoin {
- // Public key of a coin for which the wallet is requesting an abort-related refund.
- coin_pub: EddsaPublicKeyString;
-
- // The amount to be refunded (matches the original contribution)
- contribution: AmountString;
-
- // URL of the exchange this coin was withdrawn from.
- exchange_url: string;
-}
-
-export interface AbortResponse {
- // List of refund responses about the coins that the wallet
- // requested an abort for. In the same order as the 'coins'
- // from the original request.
- // The rtransaction_id is implied to be 0.
- refunds: MerchantAbortPayRefundStatus[];
-}
-
-export type MerchantAbortPayRefundStatus =
- | MerchantAbortPayRefundSuccessStatus
- | MerchantAbortPayRefundFailureStatus;
-
-// Details about why a refund failed.
-export interface MerchantAbortPayRefundFailureStatus {
- // Used as tag for the sum type RefundStatus sum type.
- type: "failure";
-
- // HTTP status of the exchange request, must NOT be 200.
- exchange_status: number;
-
- // Taler error code from the exchange reply, if available.
- exchange_code?: number;
-
- // If available, HTTP reply from the exchange.
- exchange_reply?: unknown;
-}
-
-// Additional details needed to verify the refund confirmation signature
-// (h_contract_terms and merchant_pub) are already known
-// to the wallet and thus not included.
-export interface MerchantAbortPayRefundSuccessStatus {
- // Used as tag for the sum type MerchantCoinRefundStatus sum type.
- type: "success";
-
- // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
- exchange_status: 200;
-
- // the EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
- // exchange affirming the successful refund
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: string;
-}
-
-export interface FutureKeysResponse {
- future_denoms: any[];
-
- future_signkeys: any[];
-
- master_pub: string;
-
- denom_secmod_public_key: string;
-
- // Public key of the signkey security module.
- signkey_secmod_public_key: string;
-}
-
-export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
- buildCodecForObject<FutureKeysResponse>()
- .property("master_pub", codecForString())
- .property("future_signkeys", codecForList(codecForAny()))
- .property("future_denoms", codecForList(codecForAny()))
- .property("denom_secmod_public_key", codecForAny())
- .property("signkey_secmod_public_key", codecForAny())
- .build("FutureKeysResponse");
-
-export interface MerchantConfigResponse {
- currency: string;
- name: string;
- version: string;
-}
-
-export const codecForMerchantConfigResponse =
- (): Codec<MerchantConfigResponse> =>
- buildCodecForObject<MerchantConfigResponse>()
- .property("currency", codecForString())
- .property("name", codecForString())
- .property("version", codecForString())
- .build("MerchantConfigResponse");
-
-export enum ExchangeProtocolVersion {
- /**
- * Current version supported by the wallet.
- */
- V12 = 12,
-}
-
-export enum MerchantProtocolVersion {
- /**
- * Current version supported by the wallet.
- */
- V3 = 3,
-}
-
-export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs;
-
-export interface CoinEnvelopeRsa {
- cipher: DenomKeyType.Rsa;
- rsa_blinded_planchet: string;
-}
-
-export interface CoinEnvelopeCs {
- cipher: DenomKeyType.ClauseSchnorr;
- // FIXME: add remaining fields
-}
-
-export type HashCodeString = string;
-
-export interface ExchangeWithdrawRequest {
- denom_pub_hash: HashCodeString;
- reserve_sig: EddsaSignatureString;
- coin_ev: CoinEnvelope;
-}
-
-export interface ExchangeBatchWithdrawRequest {
- planchets: ExchangeWithdrawRequest[];
-}
-
-export interface ExchangeRefreshRevealRequest {
- new_denoms_h: HashCodeString[];
- coin_evs: CoinEnvelope[];
- /**
- * kappa - 1 transfer private keys (ephemeral ECDHE keys).
- */
- transfer_privs: string[];
-
- transfer_pub: EddsaPublicKeyString;
-
- link_sigs: EddsaSignatureString[];
-
- /**
- * Iff the corresponding denomination has support for age restriction,
- * the client MUST provide the original age commitment, i.e. the vector
- * of public keys.
- */
- old_age_commitment?: Edx25519PublicKeyEnc[];
-}
-
-interface DepositConfirmationSignature {
- // The EdDSA signature of `TALER_DepositConfirmationPS` using a current
- // `signing key of the exchange <sign-key-priv>` affirming the successful
- // deposit and that the exchange will transfer the funds after the refund
- // deadline, or as soon as possible if the refund deadline is zero.
- exchange_sig: EddsaSignatureString;
-}
-
-export interface BatchDepositSuccess {
- // Optional base URL of the exchange for looking up wire transfers
- // associated with this transaction. If not given,
- // the base URL is the same as the one used for this request.
- // Can be used if the base URL for ``/transactions/`` differs from that
- // for ``/coins/``, i.e. for load balancing. Clients SHOULD
- // respect the ``transaction_base_url`` if provided. Any HTTP server
- // belonging to an exchange MUST generate a 307 or 308 redirection
- // to the correct base URL should a client uses the wrong base
- // URL, or if the base URL has changed since the deposit.
- transaction_base_url?: string;
-
- // Timestamp when the deposit was received by the exchange.
- exchange_timestamp: TalerProtocolTimestamp;
-
- // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
- // generate the signature.
- // Should match one of the exchange's signing keys from ``/keys``. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKeyString;
-
- // Array of deposit confirmation signatures from the exchange
- // Entries must be in the same order the coins were given
- // in the batch deposit request.
- exchange_sig: EddsaSignatureString;
-}
-
-export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
- buildCodecForObject<BatchDepositSuccess>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_timestamp", codecForTimestamp)
- .property("transaction_base_url", codecOptional(codecForString()))
- .build("BatchDepositSuccess");
-
-export interface TrackTransactionWired {
- // Raw wire transfer identifier of the deposit.
- wtid: Base32String;
-
- // When was the wire transfer given to the bank.
- execution_time: TalerProtocolTimestamp;
-
- // The contribution of this coin to the total (without fees)
- coin_contribution: AmountString;
-
- // Binary-only Signature_ with purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE
- // over a TALER_ConfirmWirePS
- // whereby the exchange affirms the successful wire transfer.
- exchange_sig: EddsaSignatureString;
-
- // Public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. Again given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKeyString;
-}
-
-export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> =>
- buildCodecForObject<TrackTransactionWired>()
- .property("wtid", codecForString())
- .property("execution_time", codecForTimestamp)
- .property("coin_contribution", codecForAmountString())
- .property("exchange_sig", codecForString())
- .property("exchange_pub", codecForString())
- .build("TackTransactionWired");
-
-interface TrackTransactionAccepted {
- // Legitimization target that the merchant should
- // use to check for its KYC status using
- // the /kyc-check/$REQUIREMENT_ROW/... endpoint.
- // Optional, not present if the deposit has not
- // yet been aggregated to the point that a KYC
- // need has been evaluated.
- requirement_row?: number;
-
- // True if the KYC check for the merchant has been
- // satisfied. False does not mean that KYC
- // is strictly needed, unless also a
- // legitimization_uuid is provided.
- kyc_ok: boolean;
-
- // Time by which the exchange currently thinks the deposit will be executed.
- // Actual execution may be later if the KYC check is not satisfied by then.
- execution_time: TalerProtocolTimestamp;
-}
-
-export const codecForTackTransactionAccepted =
- (): Codec<TrackTransactionAccepted> =>
- buildCodecForObject<TrackTransactionAccepted>()
- .property("requirement_row", codecOptional(codecForNumber()))
- .property("kyc_ok", codecForBoolean())
- .property("execution_time", codecForTimestamp)
- .build("TackTransactionAccepted");
-
-export type TrackTransaction =
- | ({ type: "accepted" } & TrackTransactionAccepted)
- | ({ type: "wired" } & TrackTransactionWired);
-
-export interface PurseDeposit {
- /**
- * Amount to be deposited, can be a fraction of the
- * coin's total value.
- */
- amount: AmountString;
-
- /**
- * Hash of denomination RSA key with which the coin is signed.
- */
- denom_pub_hash: HashCodeString;
-
- /**
- * Exchange's unblinded RSA signature of the coin.
- */
- ub_sig: UnblindedSignature;
-
- /**
- * Age commitment for the coin, if the denomination is age-restricted.
- */
- age_commitment?: string[];
-
- /**
- * Attestation for the minimum age, if the denomination is age-restricted.
- */
- attest?: string;
-
- /**
- * Signature over TALER_PurseDepositSignaturePS
- * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
- * made by the customer with the
- * coin's private key.
- */
- coin_sig: EddsaSignatureString;
-
- /**
- * Public key of the coin being deposited into the purse.
- */
- coin_pub: EddsaPublicKeyString;
-}
-
-export interface ExchangePurseMergeRequest {
- // payto://-URI of the account the purse is to be merged into.
- // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
- payto_uri: string;
-
- // EdDSA signature of the account/reserve affirming the merge
- // over a TALER_AccountMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
- reserve_sig: EddsaSignatureString;
-
- // EdDSA signature of the purse private key affirming the merge
- // over a TALER_PurseMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
- merge_sig: EddsaSignatureString;
-
- // Client-side timestamp of when the merge request was made.
- merge_timestamp: TalerProtocolTimestamp;
-}
-
-export interface ExchangeGetContractResponse {
- purse_pub: string;
- econtract_sig: string;
- econtract: string;
-}
-
-export const codecForExchangeGetContractResponse =
- (): Codec<ExchangeGetContractResponse> =>
- buildCodecForObject<ExchangeGetContractResponse>()
- .property("purse_pub", codecForString())
- .property("econtract_sig", codecForString())
- .property("econtract", codecForString())
- .build("ExchangeGetContractResponse");
-
-/**
- * Contract terms between two wallets (as opposed to a merchant and wallet).
- */
-export interface PeerContractTerms {
- amount: AmountString;
- summary: string;
- purse_expiration: TalerProtocolTimestamp;
-}
-
-export interface EncryptedContract {
- // Encrypted contract.
- econtract: string;
-
- // Signature over the (encrypted) contract.
- econtract_sig: string;
-
- // Ephemeral public key for the DH operation to decrypt the encrypted contract.
- contract_pub: string;
-}
-
-/**
- * Payload for /reserves/{reserve_pub}/purse
- * endpoint of the exchange.
- */
-export interface ExchangeReservePurseRequest {
- /**
- * Minimum amount that must be credited to the reserve, that is
- * the total value of the purse minus the deposit fees.
- * If the deposit fees are lower, the contribution to the
- * reserve can be higher!
- */
- purse_value: AmountString;
-
- // Minimum age required for all coins deposited into the purse.
- min_age: number;
-
- // Purse fee the reserve owner is willing to pay
- // for the purse creation. Optional, if not present
- // the purse is to be created from the purse quota
- // of the reserve.
- purse_fee: AmountString;
-
- // Optional encrypted contract, in case the buyer is
- // proposing the contract and thus establishing the
- // purse with the payment.
- econtract?: EncryptedContract;
-
- // EdDSA public key used to approve merges of this purse.
- merge_pub: EddsaPublicKeyString;
-
- // EdDSA signature of the purse private key affirming the merge
- // over a TALER_PurseMergeSignaturePS.
- // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
- merge_sig: EddsaSignatureString;
-
- // EdDSA signature of the account/reserve affirming the merge.
- // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
- reserve_sig: EddsaSignatureString;
-
- // Purse public key.
- purse_pub: EddsaPublicKeyString;
-
- // EdDSA signature of the purse over
- // TALER_PurseRequestSignaturePS of
- // purpose TALER_SIGNATURE_PURSE_REQUEST
- // confirming that the
- // above details hold for this purse.
- purse_sig: EddsaSignatureString;
-
- // SHA-512 hash of the contact of the purse.
- h_contract_terms: HashCodeString;
-
- // Client-side timestamp of when the merge request was made.
- merge_timestamp: TalerProtocolTimestamp;
-
- // Indicative time by which the purse should expire
- // if it has not been paid.
- purse_expiration: TalerProtocolTimestamp;
-}
-
-export interface ExchangePurseDeposits {
- // Array of coins to deposit into the purse.
- deposits: PurseDeposit[];
-}
-
-/**
- * @deprecated batch deposit should be used.
- */
-export interface ExchangeDepositRequest {
- // Amount to be deposited, can be a fraction of the
- // coin's total value.
- contribution: AmountString;
-
- // The merchant's account details.
- // In case of an auction policy, it refers to the seller.
- merchant_payto_uri: string;
-
- // The salt is used to hide the payto_uri from customers
- // when computing the h_wire of the merchant.
- wire_salt: string;
-
- // SHA-512 hash of the contract of the merchant with the customer. Further
- // details are never disclosed to the exchange.
- h_contract_terms: HashCodeString;
-
- // Hash of denomination RSA key with which the coin is signed.
- denom_pub_hash: HashCodeString;
-
- // Exchange's unblinded RSA signature of the coin.
- ub_sig: UnblindedSignature;
-
- // Timestamp when the contract was finalized.
- timestamp: TalerProtocolTimestamp;
-
- // Indicative time by which the exchange undertakes to transfer the funds to
- // the merchant, in case of successful payment. A wire transfer deadline of 'never'
- // is not allowed.
- wire_transfer_deadline: TalerProtocolTimestamp;
-
- // EdDSA public key of the merchant, so that the client can identify the
- // merchant for refund requests.
- //
- // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
- // policy via extension.
- merchant_pub: EddsaPublicKeyString;
-
- // Date until which the merchant can issue a refund to the customer via the
- // exchange, to be omitted if refunds are not allowed.
- //
- // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
- // policy via extension.
- refund_deadline?: TalerProtocolTimestamp;
-
- // CAVEAT: THIS IS WORK IN PROGRESS
- // (Optional) policy for the deposit.
- // This might be a refund, auction or escrow policy.
- //
- // Note that support for policies is an optional feature of the exchange.
- // Optional features are so called "extensions" in Taler. The exchange
- // provides the list of supported extensions, including policies, in the
- // ExtensionsManifestsResponse response to the /keys endpoint.
- policy?: any;
-
- // Signature over TALER_DepositRequestPS, made by the customer with the
- // coin's private key.
- coin_sig: EddsaSignatureString;
-
- h_age_commitment?: string;
-}
-
-export type WireSalt = string;
-
-export interface ExchangeBatchDepositRequest {
- // The merchant's account details.
- merchant_payto_uri: string;
-
- // The salt is used to hide the ``payto_uri`` from customers
- // when computing the ``h_wire`` of the merchant.
- wire_salt: WireSalt;
-
- // SHA-512 hash of the contract of the merchant with the customer. Further
- // details are never disclosed to the exchange.
- h_contract_terms: HashCodeString;
-
- // The list of coins that are going to be deposited with this Request.
- coins: BatchDepositRequestCoin[];
-
- // Timestamp when the contract was finalized.
- timestamp: TalerProtocolTimestamp;
-
- // Indicative time by which the exchange undertakes to transfer the funds to
- // the merchant, in case of successful payment. A wire transfer deadline of 'never'
- // is not allowed.
- wire_transfer_deadline: TalerProtocolTimestamp;
-
- // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
- // merchant for refund requests.
- merchant_pub: EddsaPublicKeyString;
-
- // Date until which the merchant can issue a refund to the customer via the
- // exchange, to be omitted if refunds are not allowed.
- //
- // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
- // policy via extension.
- refund_deadline?: TalerProtocolTimestamp;
-
- // CAVEAT: THIS IS WORK IN PROGRESS
- // (Optional) policy for the batch-deposit.
- // This might be a refund, auction or escrow policy.
- policy?: any;
-}
-
-export interface BatchDepositRequestCoin {
- // EdDSA public key of the coin being deposited.
- coin_pub: EddsaPublicKeyString;
-
- // Hash of denomination RSA key with which the coin is signed.
- denom_pub_hash: HashCodeString;
-
- // Exchange's unblinded RSA signature of the coin.
- ub_sig: UnblindedSignature;
-
- // Amount to be deposited, can be a fraction of the
- // coin's total value.
- contribution: Amounts;
-
- // Signature over `TALER_DepositRequestPS`, made by the customer with the
- // `coin's private key <coin-priv>`.
- coin_sig: EddsaSignatureString;
-
- h_age_commitment?: string;
-}
-
-export interface WalletKycUuid {
- // UUID that the wallet should use when initiating
- // the KYC check.
- requirement_row: number;
-
- // Hash of the payto:// account URI for the wallet.
- h_payto: string;
-}
-
-export const codecForWalletKycUuid = (): Codec<WalletKycUuid> =>
- buildCodecForObject<WalletKycUuid>()
- .property("requirement_row", codecForNumber())
- .property("h_payto", codecForString())
- .build("WalletKycUuid");
-
-export interface MerchantUsingTemplateDetails {
- summary?: string;
- amount?: AmountString;
-}
-
-export interface ExchangeRefundRequest {
- // Amount to be refunded, can be a fraction of the
- // coin's total deposit value (including deposit fee);
- // must be larger than the refund fee.
- refund_amount: AmountString;
-
- // SHA-512 hash of the contact of the merchant with the customer.
- h_contract_terms: HashCodeString;
-
- // 64-bit transaction id of the refund transaction between merchant and customer.
- rtransaction_id: number;
-
- // EdDSA public key of the merchant.
- merchant_pub: EddsaPublicKeyString;
-
- // EdDSA signature of the merchant over a
- // TALER_RefundRequestPS with purpose
- // TALER_SIGNATURE_MERCHANT_REFUND
- // affirming the refund.
- merchant_sig: EddsaPublicKeyString;
-}
-
-export interface ExchangeRefundSuccessResponse {
- // The EdDSA :ref:signature (binary-only) with purpose
- // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
- // a TALER_RecoupRefreshConfirmationPS
- // using a current signing key of the
- // exchange affirming the successful refund.
- exchange_sig: EddsaSignatureString;
-
- // Public EdDSA key of the exchange that was used to generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
- // explicitly as the client might otherwise be confused by clock skew as to
- // which signing key was used.
- exchange_pub: EddsaPublicKeyString;
-}
-
-export const codecForExchangeRefundSuccessResponse =
- (): Codec<ExchangeRefundSuccessResponse> =>
- buildCodecForObject<ExchangeRefundSuccessResponse>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .build("ExchangeRefundSuccessResponse");
-
-export type AccountRestriction =
- | RegexAccountRestriction
- | DenyAllAccountRestriction;
-
-export interface DenyAllAccountRestriction {
- type: "deny";
-}
-
-// Accounts interacting with this type of account
-// restriction must have a payto://-URI matching
-// the given regex.
-export 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?: InternationalizedString;
-}
-
-export interface ExchangeWireAccount {
- // 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: EddsaSignatureString;
-
- // Display label wallets should use to show this
- // bank account.
- // Since protocol **v19**.
- bank_label?: string;
- priority?: number;
-}
-
-export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> =>
- buildCodecForObject<ExchangeWireAccount>()
- .property("conversion_url", codecOptional(codecForStringURL()))
- .property("credit_restrictions", codecForList(codecForAny()))
- .property("debit_restrictions", codecForList(codecForAny()))
- .property("master_sig", codecForString())
- .property("payto_uri", codecForString())
- .property("bank_label", codecOptional(codecForString()))
- .property("priority", codecOptional(codecForNumber()))
- .build("WireAccount");
-
-export type Integer = number;
-
-export interface BankConversionInfoConfig {
- // libtool-style representation of the Bank 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 API.
- name: "taler-conversion-info";
-
- regional_currency: string;
-
- fiat_currency: string;
-
- // Currency used by this bank.
- regional_currency_specification: CurrencySpecification;
-
- // External currency used during conversion.
- fiat_currency_specification: CurrencySpecification;
-}
-
-export const codecForBankConversionInfoConfig =
- (): Codec<BankConversionInfoConfig> =>
- buildCodecForObject<BankConversionInfoConfig>()
- .property("name", codecForConstString("taler-conversion-info"))
- .property("version", codecForString())
- .property("fiat_currency", codecForString())
- .property("regional_currency", codecForString())
- .property("fiat_currency_specification", codecForCurrencySpecificiation())
- .property(
- "regional_currency_specification",
- codecForCurrencySpecificiation(),
- )
- .build("BankConversionInfoConfig");
-
-export interface DenominationExpiredMessage {
- // Taler error code. Note that beyond
- // expiration this message format is also
- // used if the key is not yet valid, or
- // has been revoked.
- code: number;
-
- // Signature by the exchange over a
- // TALER_DenominationExpiredAffirmationPS.
- // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
- exchange_sig: EddsaSignatureString;
-
- // Public key of the exchange used to create
- // the 'exchange_sig.
- exchange_pub: EddsaPublicKeyString;
-
- // Hash of the denomination public key that is unknown.
- h_denom_pub: HashCodeString;
-
- // When was the signature created.
- timestamp: TalerProtocolTimestamp;
-
- // What kind of operation was requested that now
- // failed?
- oper: string;
-}
-
-export const codecForDenominationExpiredMessage = () =>
- buildCodecForObject<DenominationExpiredMessage>()
- .property("code", codecForNumber())
- .property("exchange_sig", codecForString())
- .property("exchange_pub", codecForString())
- .property("h_denom_pub", codecForString())
- .property("timestamp", codecForTimestamp)
- .property("oper", codecForString())
- .build("DenominationExpiredMessage");
-
-export interface CoinHistoryResponse {
- // Current balance of the coin.
- balance: AmountString;
-
- // Hash of the coin's denomination.
- h_denom_pub: HashCodeString;
-
- // Transaction history for the coin.
- history: any[];
-}
-
-export const codecForCoinHistoryResponse = () =>
- buildCodecForObject<CoinHistoryResponse>()
- .property("balance", codecForAmountString())
- .property("h_denom_pub", codecForString())
- .property("history", codecForAny())
- .build("CoinHistoryResponse");
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index b92366fb3..694260330 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -15,7 +15,7 @@
*/
import test from "ava";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
import {
parseAddExchangeUri,
parseDevExperimentUri,
@@ -54,6 +54,18 @@ test("taler withdraw uri parsing", (t) => {
t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
});
+test("taler withdraw uri parsing with external confirmation", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.externalConfirmation, true);
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+});
+
test("taler withdraw uri parsing (http)", (t) => {
const url1 = "taler+http://withdraw/bank.example.com/12345";
const r1 = parseWithdrawUri(url1);
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 54b7525e3..b4455fba4 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -27,8 +27,9 @@ import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
import { TalerErrorCode } from "./taler-error-codes.js";
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
import { URL, URLSearchParams } from "./url.js";
+
/**
* A parsed taler URI.
*/
@@ -89,6 +90,7 @@ export interface WithdrawUriResult {
type: TalerUriAction.Withdraw;
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
+ externalConfirmation?: boolean;
}
export interface RefundUriResult {
@@ -140,7 +142,12 @@ export function parseWithdrawUriWithError(s: string) {
if (pi.type === "fail") {
return pi;
}
- const parts = pi.body.rest.split("/");
+
+ const c = pi.body.rest.split("?", 2);
+ const path = c[0];
+ const q = new URLSearchParams(c[1] ?? "");
+
+ const parts = path.split("/");
if (parts.length < 2) {
return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
@@ -166,6 +173,7 @@ export function parseWithdrawUriWithError(s: string) {
`${pi.body.innerProto}://${p}/`,
),
withdrawalOperationId: withdrawId,
+ externalConfirmation: q.get("external-confirmation") == "1",
};
return opFixedSuccess(result);
}
diff --git a/packages/taler-util/src/transaction-test-data.ts b/packages/taler-util/src/transaction-test-data.ts
index 378028144..85f4684bb 100644
--- a/packages/taler-util/src/transaction-test-data.ts
+++ b/packages/taler-util/src/transaction-test-data.ts
@@ -18,8 +18,8 @@ import {
TransactionType,
PaymentStatus,
TransactionMajorState,
-} from "./transactions-types.js";
-import { RefreshReason } from "./wallet-types.js";
+} from "./types-taler-wallet-transactions.js";
+import { RefreshReason } from "./types-taler-wallet.js";
/**
* Sample transaction list entries.
diff --git a/packages/taler-util/src/twrpc.ts b/packages/taler-util/src/twrpc.ts
index d221630d0..368e04e27 100644
--- a/packages/taler-util/src/twrpc.ts
+++ b/packages/taler-util/src/twrpc.ts
@@ -14,8 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CoreApiResponse } from "./wallet-types.js";
-
/**
* Implementation for the wallet-core IPC protocol.
*
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/taler-util/src/type-override.d.ts
index 707324d40..703b60331 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx
+++ b/packages/taler-util/src/type-override.d.ts
@@ -15,14 +15,18 @@
*/
/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
+ * define unknown type of catch function
*/
-
-import { FunctionalComponent, h } from "preact";
-import { ListPage as TestedComponent } from "./ListPage.js";
-
-export default {
- title: "Pages/Templates/List",
- component: TestedComponent,
-};
+interface Promise<T> {
+ /**
+ * Attaches a callback for only the rejection of the Promise.
+ * @param onrejected The callback to execute when the Promise is rejected.
+ * @returns A Promise for the completion of the callback.
+ */
+ catch<TResult = never>(
+ onrejected?:
+ | ((reason: unknown) => TResult | PromiseLike<TResult>)
+ | undefined
+ | null,
+ ): Promise<T | TResult>;
+}
diff --git a/packages/taler-util/src/types-taler-bank-conversion.ts b/packages/taler-util/src/types-taler-bank-conversion.ts
new file mode 100644
index 000000000..31cfa5aad
--- /dev/null
+++ b/packages/taler-util/src/types-taler-bank-conversion.ts
@@ -0,0 +1,200 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { codecForAmountString } from "./amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForConstString,
+ codecForEither,
+ codecForString,
+} from "./codec.js";
+import {
+ AmountString,
+ CurrencySpecification,
+ DecimalNumber,
+ codecForCurrencySpecificiation,
+ codecForDecimalNumber,
+} from "./types-taler-common.js";
+
+export interface ConversionInfo {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: "zero" | "up" | "nearest";
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: "zero" | "up" | "nearest";
+}
+
+export interface TalerConversionInfoConfig {
+ // libtool-style representation of the Bank 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 API.
+ name: "taler-conversion-info";
+
+ // Currency used by this bank.
+ regional_currency: string;
+
+ // How the bank SPA should render this currency.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency: string;
+
+ // How the bank SPA should render this currency.
+ fiat_currency_specification: CurrencySpecification;
+
+ // Extra conversion rate information.
+ // Only present if server opts in to report the static conversion rate.
+ conversion_rate: ConversionInfo;
+}
+
+export interface CashinConversionResponse {
+ // Amount that the user will get deducted from their fiat
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their regional
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+}
+
+export interface CashoutConversionResponse {
+ // Amount that the user will get deducted from their regional
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their fiat
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+}
+
+export type RoundingMode = "zero" | "up" | "nearest";
+
+export interface ConversionRate {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: RoundingMode;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: RoundingMode;
+}
+
+export const codecForCashoutConversionResponse =
+ (): Codec<CashoutConversionResponse> =>
+ buildCodecForObject<CashoutConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashoutConversionResponse");
+
+export const codecForCashinConversionResponse =
+ (): Codec<CashinConversionResponse> =>
+ buildCodecForObject<CashinConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashinConversionResponse");
+
+export const codecForConversionInfo = (): Codec<ConversionInfo> =>
+ buildCodecForObject<ConversionInfo>()
+ .property("cashin_fee", codecForAmountString())
+ .property("cashin_min_amount", codecForAmountString())
+ .property("cashin_ratio", codecForDecimalNumber())
+ .property(
+ "cashin_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashin_tiny_amount", codecForAmountString())
+ .property("cashout_fee", codecForAmountString())
+ .property("cashout_min_amount", codecForAmountString())
+ .property("cashout_ratio", codecForDecimalNumber())
+ .property(
+ "cashout_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashout_tiny_amount", codecForAmountString())
+ .build("ConversionBankConfig.ConversionInfo");
+
+export const codecForConversionBankConfig = (): Codec<TalerConversionInfoConfig> =>
+ buildCodecForObject<TalerConversionInfoConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("regional_currency", codecForString())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .property("fiat_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+
+ .property("conversion_rate", codecForConversionInfo())
+ .build("ConversionBankConfig.IntegrationConfig");
diff --git a/packages/taler-util/src/types-taler-bank-integration.ts b/packages/taler-util/src/types-taler-bank-integration.ts
new file mode 100644
index 000000000..02161474f
--- /dev/null
+++ b/packages/taler-util/src/types-taler-bank-integration.ts
@@ -0,0 +1,193 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { Codec, buildCodecForObject, codecForConstString, codecForEither, codecOptional } from "./codec.js";
+import { codecForAmountString, codecForList, codecForString } from "./index.js";
+import { PaytoString, codecForPaytoString } from "./payto.js";
+import { AmountString, CurrencySpecification, codecForCurrencyName, codecForCurrencySpecificiation, codecForLibtoolVersion, codecForURLString } from "./types-taler-common.js";
+
+export type WithdrawalOperationStatus =
+ | "pending"
+ | "selected"
+ | "aborted"
+ | "confirmed";
+
+export interface BankVersion {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification?: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+}
+
+export interface BankWithdrawalOperationStatus {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Currency used for the withdrawal.
+ // MUST be present when amount is absent.
+ // @since v2, may become mandatory in the future.
+ currency?: string;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations). Only
+ // given once the amount is fixed and cannot be changed.
+ // Optional since **vC2EC**.
+ amount?: AmountString | undefined;
+
+ // Suggestion for the amount to be withdrawn with this
+ // operation. Given if a suggestion was made but the
+ // user may still change the amount.
+ // Optional since **vC2EC**.
+ suggested_amount?: AmountString | undefined;
+
+ // Maximum amount that the wallet can choose to withdraw.
+ // Only applicable when the amount is not fixed.
+ // @since **vC2EC**.
+ max_amount?: AmountString | undefined;
+
+ // The non-Taler card fees the customer will have
+ // to pay to the bank / payment service provider
+ // they are using to make the withdrawal.
+ // @since **vC2EC**
+ card_fees?: AmountString | undefined;
+
+ // Bank account of the customer that is debiting, as an
+ // RFC 8905 payto URI.
+ sender_wire?: PaytoString;
+
+ // Base URL of the suggested exchange. The bank may have
+ // neither a suggestion nor a requirement for the exchange.
+ // This value is typically set in the bank's configuration.
+ suggested_exchange?: string;
+
+ // Base URL of an exchange that must be used. Optional,
+ // not given *unless* a particular exchange is mandatory.
+ // This value is typically set in the bank's configuration.
+ // @since **vC2EC**
+ required_exchange?: string;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ // Only applicable when status is selected or pending.
+ // It may contain the withdrawal operation id.
+ confirm_transfer_url?: string;
+
+ // Wire transfer types supported by the bank.
+ wire_types: string[];
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet;
+ // only non-null if status is selected or confirmed.
+ // @since **v1**
+ selected_exchange_account?: string;
+}
+
+export interface BankWithdrawalOperationPostRequest {
+ // Reserve public key that should become the wire transfer
+ // subject to fund the withdrawal.
+ reserve_pub: string;
+
+ // Payto address of the exchange selected for the withdrawal.
+ selected_exchange: PaytoString;
+
+ // Selected amount to be transferred. Optional if the
+ // backend already knows the amount.
+ // @since **vC2EC**
+ amount?: AmountString | undefined;
+}
+
+export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: Omit<"pending", WithdrawalOperationStatus>;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected or pending.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+}
+
+
+export const codecForBankVersion =
+ (): Codec<BankVersion> =>
+ buildCodecForObject<BankVersion>()
+ .property("currency", codecForCurrencyName())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForLibtoolVersion())
+ .build("TalerBankIntegrationApi.BankVersion");
+
+export const codecForBankWithdrawalOperationStatus =
+ (): Codec<BankWithdrawalOperationStatus> =>
+ buildCodecForObject<BankWithdrawalOperationStatus>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("currency", codecOptional(codecForCurrencyName()))
+ .property("suggested_amount", codecOptional(codecForAmountString()))
+ .property("card_fees", codecOptional(codecForAmountString()))
+ .property("sender_wire", codecOptional(codecForPaytoString()))
+ .property("suggested_exchange", codecOptional(codecForURLString()))
+ .property("confirm_transfer_url", codecOptional(codecForURLString()))
+ .property("wire_types", codecForList(codecForString()))
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property("selected_exchange_account", codecOptional(codecForString()))
+ .property("max_amount", codecOptional(codecForAmountString()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationStatus");
+
+export const codecForBankWithdrawalOperationPostResponse =
+ (): Codec<BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForURLString()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse"); \ No newline at end of file
diff --git a/packages/taler-util/src/types-taler-challenger.ts b/packages/taler-util/src/types-taler-challenger.ts
new file mode 100644
index 000000000..70ac35ee9
--- /dev/null
+++ b/packages/taler-util/src/types-taler-challenger.ts
@@ -0,0 +1,293 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import { TalerProtocolTimestamp, codecForTimestamp } from "./time.js";
+import {
+ Integer,
+ InternationalizedString,
+ Timestamp,
+} from "./types-taler-common.js";
+
+export interface ChallengerTermsOfServiceResponse {
+ // Name of the service
+ name: "challenger";
+
+ // libtool-style representation of the Challenger protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // @since v2.
+ address_type: "email" | "phone";
+}
+
+export interface ChallengeSetupResponse {
+ // Nonce to use when constructing /authorize endpoint.
+ nonce: string;
+}
+
+export interface Restriction {
+ regex?: string;
+ hint?: string;
+ hint_i18n?: InternationalizedString;
+}
+
+export interface ChallengeStatus {
+ // indicates if the given address cannot be changed anymore, the
+ // form should be read-only if set to true.
+ fix_address: boolean;
+
+ // form values from the previous submission if available, details depend
+ // on the ADDRESS_TYPE, should be used to pre-populate the form
+ last_address: Record<string, string> | undefined;
+
+ // number of times the address can still be changed, may or may not be
+ // shown to the user
+ changes_left: Integer;
+
+ // is the challenge already solved?
+ solved: boolean;
+
+ // when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ // only present if challenge already created
+ // @since v2
+ retransmission_time: Timestamp;
+
+ // how many times might the PIN still be retransmitted
+ // only present if challenge already created
+ // @since v2
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ // only present if challenge already created
+ // @since v2
+ auth_attempts_left: Integer;
+}
+
+export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse;
+
+export interface ChallengeRedirect {
+ type: "completed";
+ // challenge is completed, use should redirect here
+ redirect_url: string;
+}
+
+export interface ChallengeCreateResponse {
+ type: "created";
+ // how many more attempts are allowed, might be shown to the user,
+ // highlighting might be appropriate for low values such as 1 or 2 (the
+ // form will never be used if the value is zero)
+ attempts_left: Integer;
+
+ // the address that is being validated, might be shown or not
+ address: Object;
+
+ // true if we just retransmitted the challenge, false if we sent a
+ // challenge recently and thus refused to transmit it again this time;
+ // might make a useful hint to the user
+ transmitted: boolean;
+
+ // timestamp explaining when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ retransmission_time: TalerProtocolTimestamp;
+}
+
+export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse;
+
+export interface InvalidPinResponse {
+ type: "pending";
+
+ // numeric Taler error code, should be shown to indicate the error
+ // compactly for reporting to developers
+ ec?: number;
+
+ // human-readable Taler error code, should be shown for the user to
+ // understand the error
+ hint: string;
+
+ // how many times is the user still allowed to change the address;
+ // if 0, the user should not be shown a link to jump to the
+ // address entry form
+ addresses_left: Integer;
+
+ // how many times might the PIN still be retransmitted
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ auth_attempts_left: Integer;
+
+ // if true, the PIN was not even evaluated as the user previously
+ // exhausted the number of attempts
+ exhausted: boolean;
+
+ // if true, the PIN was not even evaluated as no challenge was ever
+ // issued (the user must have skipped the step of providing their
+ // address first!)
+ no_challenge: boolean;
+}
+
+export interface ChallengerAuthResponse {
+ // Token used to authenticate access in /info.
+ access_token: string;
+
+ // Type of the access token.
+ token_type: "Bearer";
+
+ // Amount of time that an access token is valid (in seconds).
+ expires_in: Integer;
+}
+
+export interface ChallengerInfoResponse {
+ // Unique ID of the record within Challenger
+ // (identifies the rowid of the token).
+ id: Integer;
+
+ // Address that was validated.
+ // Key-value pairs, details depend on the
+ // address_type.
+ address: Object;
+
+ // Type of the address.
+ address_type: string;
+
+ // How long do we consider the address to be
+ // valid for this user.
+ expires: Timestamp;
+}
+
+export const codecForChallengerTermsOfServiceResponse =
+ (): Codec<ChallengerTermsOfServiceResponse> =>
+ buildCodecForObject<ChallengerTermsOfServiceResponse>()
+ .property("name", codecForConstString("challenger"))
+ .property("version", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property(
+ "address_type",
+ codecForEither(
+ codecForConstString("phone"),
+ codecForConstString("email"),
+ ),
+ )
+ .build("ChallengerApi.ChallengerTermsOfServiceResponse");
+
+export const codecForChallengeSetupResponse =
+ (): Codec<ChallengeSetupResponse> =>
+ buildCodecForObject<ChallengeSetupResponse>()
+ .property("nonce", codecForString())
+ .build("ChallengerApi.ChallengeSetupResponse");
+
+export const codecForChallengeStatus = (): Codec<ChallengeStatus> =>
+ buildCodecForObject<ChallengeStatus>()
+ .property("fix_address", codecForBoolean())
+ .property("solved", codecForBoolean())
+ .property("last_address", codecOptional(codecForMap(codecForAny())))
+ .property("changes_left", codecForNumber())
+ .property("retransmission_time", codecForTimestamp)
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .build("ChallengerApi.ChallengeStatus");
+
+export const codecForChallengeResponse = (): Codec<ChallengeResponse> =>
+ buildCodecForUnion<ChallengeResponse>()
+ .discriminateOn("type")
+ .alternative("completed", codecForChallengeRedirect())
+ .alternative("created", codecForChallengeCreateResponse())
+ .build("ChallengerApi.ChallengeResponse");
+
+export const codecForChallengeCreateResponse =
+ (): Codec<ChallengeCreateResponse> =>
+ buildCodecForObject<ChallengeCreateResponse>()
+ .property("attempts_left", codecForNumber())
+ .property("type", codecForConstString("created"))
+ .property("address", codecForAny())
+ .property("transmitted", codecForBoolean())
+ .property("retransmission_time", codecForTimestamp)
+ .build("ChallengerApi.ChallengeCreateResponse");
+
+export const codecForChallengeRedirect = (): Codec<ChallengeRedirect> =>
+ buildCodecForObject<ChallengeRedirect>()
+ .property("type", codecForConstString("completed"))
+ .property("redirect_url", codecForString())
+ .build("ChallengerApi.ChallengeRedirect");
+
+export const codecForChallengeInvalidPinResponse =
+ (): Codec<InvalidPinResponse> =>
+ buildCodecForObject<InvalidPinResponse>()
+ .property("ec", codecOptional(codecForNumber()))
+ .property("hint", codecForAny())
+ .property("type", codecForConstString("pending"))
+ .property("addresses_left", codecForNumber())
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .property("exhausted", codecForBoolean())
+ .property("no_challenge", codecForBoolean())
+ .build("ChallengerApi.InvalidPinResponse");
+
+export const codecForChallengeSolveResponse =
+ (): Codec<ChallengeSolveResponse> =>
+ buildCodecForUnion<ChallengeSolveResponse>()
+ .discriminateOn("type")
+ .alternative("completed", codecForChallengeRedirect())
+ .alternative("pending", codecForChallengeInvalidPinResponse())
+ .build("ChallengerApi.ChallengeSolveResponse");
+
+export const codecForChallengerAuthResponse =
+ (): Codec<ChallengerAuthResponse> =>
+ buildCodecForObject<ChallengerAuthResponse>()
+ .property("access_token", codecForString())
+ .property("token_type", codecForAny())
+ .property("expires_in", codecForNumber())
+ .build("ChallengerApi.ChallengerAuthResponse");
+
+export const codecForChallengerInfoResponse =
+ (): Codec<ChallengerInfoResponse> =>
+ buildCodecForObject<ChallengerInfoResponse>()
+ .property("id", codecForNumber())
+ .property("address", codecForAny())
+ .property("address_type", codecForString())
+ .property("expires", codecForTimestamp)
+ .build("ChallengerApi.ChallengerInfoResponse");
diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts
new file mode 100644
index 000000000..dd983cf2e
--- /dev/null
+++ b/packages/taler-util/src/types-taler-common.ts
@@ -0,0 +1,559 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Type and schema definitions and helpers for the core GNU Taler protocol.
+ *
+ * Even though the rest of the wallet uses camelCase for fields, use snake_case
+ * here, since that's the convention for the Taler JSON+HTTP API.
+ */
+
+/**
+ * Imports.
+ */
+
+import { codecForAmountString } from "./amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import { ReservePub, codecForEither } from "./index.js";
+import {
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForTimestamp,
+} from "./time.js";
+
+// 64-byte hash code.
+export type HashCode = string;
+
+export type PaytoHash = string;
+
+export type AmlOfficerPublicKeyP = string;
+
+// 32-byte hash code.
+export type ShortHashCode = string;
+
+export type SHA256HashCode = ShortHashCode;
+
+export type SHA512HashCode = HashCode;
+
+// 32-byte nonce value, must only be used once.
+export type CSNonce = string;
+
+// 32-byte nonce value, must only be used once.
+export type RefreshMasterSeed = string;
+
+// 32-byte value representing a scalar multiplier
+// for scalar operations on points on Curve25519.
+export type Cs25519Scalar = string;
+
+///
+/// KEYS
+///
+
+// 16-byte access token used to authorize access.
+export type ClaimToken = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+export type EddsaPublicKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+export type EddsaPrivateKey = string;
+
+// Edx25519 public keys are points on Curve25519 and represented using the
+// standard 256 bits Ed25519 compact format converted to Crockford
+// Base32.
+//export type Edx25519PublicKey = string;
+
+// Edx25519 private keys are always points on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+//export type Edx25519PrivateKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+export type EcdhePublicKey = string;
+
+// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+export type CsRPublic = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+export type EcdhePrivateKey = string;
+
+export type CoinPublicKey = EddsaPublicKey;
+
+// RSA public key converted to Crockford Base32.
+export type RsaPublicKey = string;
+
+export type WireTransferIdentifierRawP = string;
+// Subset of numbers: Integers in the
+// inclusive range 0 .. (2^53 - 1).
+export type SafeUint64 = number;
+
+export type WadId = string;
+
+export type Timestamp = TalerProtocolTimestamp;
+
+export type RelativeTime = TalerProtocolDuration;
+
+export type RsaSignature = string;
+
+export type BlindedRsaSignature = string;
+
+/**
+ * DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
+ */
+export interface CurrencySpecification {
+ // Name of the currency.
+ name: string;
+
+ // how many digits the user may enter after the decimal_separator
+ num_fractional_input_digits: Integer;
+
+ // Number of fractional digits to render in normal font and size.
+ num_fractional_normal_digits: Integer;
+
+ // Number of fractional digits to render always, if needed by
+ // padding with zeros.
+ num_fractional_trailing_zero_digits: Integer;
+
+ // map of powers of 10 to alternative currency names / symbols, must
+ // always have an entry under "0" that defines the base name,
+ // e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC".
+ // Communicates the currency symbol to be used.
+ alt_unit_names: { [log10: string]: string };
+}
+
+export interface InternationalizedString {
+ [lang_tag: string]: string;
+}
+
+export type RsaPublicKeySring = string;
+export type AgeMask = number;
+
+// The string must be a data URL according to RFC 2397
+// with explicit mediatype and base64 parameters.
+//
+// data:<mediatype>;base64,<data>
+//
+// Supported mediatypes are image/jpeg and image/png.
+// Invalid strings will be rejected by the wallet.
+export type ImageDataUrl = string;
+
+/**
+ * 32-byte value representing a point on Curve25519.
+ */
+export type Cs25519Point = string;
+
+/**
+ * Response from the bank.
+ */
+export class WithdrawOperationStatusResponse {
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
+ selection_done: boolean;
+
+ transfer_done: boolean;
+
+ aborted: boolean;
+
+ amount: string | undefined;
+
+ sender_wire?: string;
+
+ suggested_exchange?: string;
+
+ confirm_transfer_url?: string;
+
+ wire_types: string[];
+}
+
+export type LitAmountString = `${string}:${number}`;
+
+export type LibtoolVersionString = string;
+
+export type DecimalNumber = string;
+
+declare const __amount_str: unique symbol;
+export type AmountString =
+ | (string & { [__amount_str]: true })
+ | LitAmountString;
+// export type AmountString = string;
+export type Base32String = string;
+export type EddsaSignatureString = string;
+export type EddsaPublicKeyString = string;
+export type EddsaPrivateKeyString = string;
+export type CoinPublicKeyString = string;
+
+// FIXME: implement this codec
+export const codecForURLString = codecForString;
+// FIXME: implement this codec
+export const codecForLibtoolVersion = codecForString;
+// FIXME: implement this codec
+export const codecForCurrencyName = codecForString;
+// FIXME: implement this codec
+export const codecForDecimalNumber = codecForString;
+
+export const codecForInternationalizedString =
+ (): Codec<InternationalizedString> => codecForMap(codecForString());
+
+export const codecForWithdrawOperationStatusResponse =
+ (): Codec<WithdrawOperationStatusResponse> =>
+ buildCodecForObject<WithdrawOperationStatusResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
+ .property("selection_done", codecForBoolean())
+ .property("transfer_done", codecForBoolean())
+ .property("aborted", codecForBoolean())
+ .property("amount", codecOptional(codecForString()))
+ .property("sender_wire", codecOptional(codecForString()))
+ .property("suggested_exchange", codecOptional(codecForString()))
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .property("wire_types", codecForList(codecForString()))
+ .build("WithdrawOperationStatusResponse");
+
+export const codecForCurrencySpecificiation =
+ (): Codec<CurrencySpecification> =>
+ buildCodecForObject<CurrencySpecification>()
+ .property("name", codecForString())
+ .property("num_fractional_input_digits", codecForNumber())
+ .property("num_fractional_normal_digits", codecForNumber())
+ .property("num_fractional_trailing_zero_digits", codecForNumber())
+ .property("alt_unit_names", codecForMap(codecForString()))
+ .build("CurrencySpecification");
+
+export interface TalerCommonConfigResponse {
+ name: string;
+ version: string;
+}
+
+export const codecForTalerCommonConfigResponse =
+ (): Codec<TalerCommonConfigResponse> =>
+ buildCodecForObject<TalerCommonConfigResponse>()
+ .property("name", codecForString())
+ .property("version", codecForString())
+ .build("TalerCommonConfigResponse");
+
+export enum ExchangeProtocolVersion {
+ /**
+ * Current version supported by the wallet.
+ */
+ V12 = 12,
+}
+
+export enum MerchantProtocolVersion {
+ /**
+ * Current version supported by the wallet.
+ */
+ V3 = 3,
+}
+
+export type HashCodeString = string;
+
+export type WireSalt = string;
+
+export interface MerchantUsingTemplateDetails {
+ summary?: string;
+ amount?: AmountString;
+}
+
+export type Integer = number;
+
+export interface BankConversionInfoConfig {
+ // libtool-style representation of the Bank 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 API.
+ name: "taler-conversion-info";
+
+ regional_currency: string;
+
+ fiat_currency: string;
+
+ // Currency used by this bank.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency_specification: CurrencySpecification;
+}
+
+export const codecForBankConversionInfoConfig =
+ (): Codec<BankConversionInfoConfig> =>
+ buildCodecForObject<BankConversionInfoConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("fiat_currency", codecForString())
+ .property("regional_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .build("BankConversionInfoConfig");
+
+export interface DenominationExpiredMessage {
+ // Taler error code. Note that beyond
+ // expiration this message format is also
+ // used if the key is not yet valid, or
+ // has been revoked.
+ code: number;
+
+ // Signature by the exchange over a
+ // TALER_DenominationExpiredAffirmationPS.
+ // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
+ exchange_sig: EddsaSignatureString;
+
+ // Public key of the exchange used to create
+ // the 'exchange_sig.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Hash of the denomination public key that is unknown.
+ h_denom_pub: HashCodeString;
+
+ // When was the signature created.
+ timestamp: TalerProtocolTimestamp;
+
+ // What kind of operation was requested that now
+ // failed?
+ oper: string;
+}
+
+export const codecForDenominationExpiredMessage = () =>
+ buildCodecForObject<DenominationExpiredMessage>()
+ .property("code", codecForNumber())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("h_denom_pub", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("oper", codecForString())
+ .build("DenominationExpiredMessage");
+
+export interface CoinHistoryResponse {
+ // Current balance of the coin.
+ balance: AmountString;
+
+ // Hash of the coin's denomination.
+ h_denom_pub: HashCodeString;
+
+ // Transaction history for the coin.
+ history: any[];
+}
+
+export const codecForCoinHistoryResponse = () =>
+ buildCodecForObject<CoinHistoryResponse>()
+ .property("balance", codecForAmountString())
+ .property("h_denom_pub", codecForString())
+ .property("history", codecForAny())
+ .build("CoinHistoryResponse");
+
+export interface TokenRequest {
+ // Service-defined scope for the token.
+ // Typical scopes would be "readonly" or "readwrite".
+ scope: string;
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Is the token refreshable into a new token during its
+ // validity?
+ // Refreshable tokens effectively provide indefinite
+ // access if they are refreshed in time.
+ refreshable?: boolean;
+}
+
+export interface TokenSuccessResponse {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ access_token: AccessToken;
+}
+export interface TokenSuccessResponseMerchant {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ token: AccessToken;
+}
+
+//FIXME: implement this codec
+export const codecForAccessToken = codecForString as () => Codec<AccessToken>;
+export const codecForTokenSuccessResponse = (): Codec<TokenSuccessResponse> =>
+ buildCodecForObject<TokenSuccessResponse>()
+ .property("access_token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponse");
+
+export const codecForTokenSuccessResponseMerchant =
+ (): Codec<TokenSuccessResponseMerchant> =>
+ buildCodecForObject<TokenSuccessResponseMerchant>()
+ .property("token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponseMerchant");
+
+// FIXME: implement this codec
+export const codecForURN = codecForString;
+
+declare const __ac_token: unique symbol;
+
+/**
+ * Use `createAccessToken(string)` function to build one.
+ */
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ * Encode the token with rfc7230 to send in a http header.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenEncoded(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:")
+ ? token
+ : `secret-token:${encodeURIComponent(token)}`
+ ) as AccessToken;
+}
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenPlain(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+
+/**
+ * Convert string to access token.
+ *
+ * @param clientSecret
+ * @returns
+ */
+export function createClientSecretAccessToken(
+ clientSecret: string,
+): AccessToken {
+ return clientSecret as AccessToken;
+}
+
+export type UserAndPassword = {
+ username: string;
+ password: string;
+};
+
+export type UserAndToken = {
+ username: string;
+ token: AccessToken;
+};
+
+declare const opaque_OfficerAccount: unique symbol;
+/**
+ * Sealed private key for AML officer
+ */
+export type LockedAccount = string & { [opaque_OfficerAccount]: true };
+
+declare const opaque_OfficerId: unique symbol;
+/**
+ * Public key for AML officer
+ */
+export type OfficerId = string & { [opaque_OfficerId]: true };
+
+declare const opaque_OfficerSigningKey: unique symbol;
+/**
+ * Private key for AML officer
+ */
+export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true };
+
+export interface OfficerAccount {
+ id: OfficerId;
+ signingKey: SigningKey;
+}
+
+export interface ReserveAccount {
+ id: ReservePub;
+ signingKey: SigningKey;
+}
+
+export type PaginationParams = {
+ /**
+ * row identifier as the starting point of the query
+ */
+ offset?: string;
+ /**
+ * max number of element in the result response
+ * always greater than 0
+ */
+ limit?: number;
+ /**
+ * order
+ */
+ order?: "asc" | "dec";
+};
+
+export type LongPollParams = {
+ /**
+ * milliseconds the server should wait for at least one result to be shown
+ */
+ timeoutMs?: number;
+};
+
+export interface LoginToken {
+ token: AccessToken;
+ expiration: Timestamp;
+}
diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts
new file mode 100644
index 000000000..994623e4d
--- /dev/null
+++ b/packages/taler-util/src/types-taler-corebank.ts
@@ -0,0 +1,916 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForConstString,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import {
+ buildCodecForUnion,
+ codecForAmountString,
+ codecForEither,
+ codecForList,
+ codecForNumber,
+ codecForTalerUriString,
+ codecForTimestamp,
+ codecOptionalDefault,
+} from "./index.js";
+import { PaytoString, codecForPaytoString } from "./payto.js";
+import { TalerUriString } from "./taleruri.js";
+import { WithdrawalOperationStatus } from "./types-taler-bank-integration.js";
+import {
+ AmountString,
+ CurrencySpecification,
+ DecimalNumber,
+ Integer,
+ ShortHashCode,
+ Timestamp,
+ codecForCurrencySpecificiation,
+ codecForDecimalNumber,
+} from "./types-taler-common.js";
+
+export interface IntegrationConfig {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+}
+
+export interface TalerCorebankConfigResponse {
+ /**
+ * Name of this API, always "taler-corebank".
+ *
+ * For legacy reasons, libeufin-bank will also be accepted for some time.
+ */
+ name: "libeufin-bank" | "taler-corebank";
+
+ // API version in the form $n:$n:$n
+ version: string;
+
+ // Bank display name to be used in user interfaces.
+ // For consistency use "Taler Bank" if missing.
+ // @since v4, will become mandatory in the next version.
+ bank_name?: string;
+
+ // Advertised base URL to use when you sharing an URL with another
+ // program.
+ // @since v4.
+ base_url?: string;
+
+ // If 'true' the server provides local currency conversion support
+ // If 'false' some parts of the API are not supported and return 501
+ allow_conversion?: boolean;
+
+ // If 'true' anyone can register
+ // If 'false' only the admin can
+ allow_registrations?: boolean;
+
+ // If 'true' account can delete themselves
+ // If 'false' only the admin can delete accounts
+ allow_deletions?: boolean;
+
+ // If 'true' anyone can edit their name
+ // If 'false' only admin can
+ allow_edit_name?: boolean;
+
+ // If 'true' anyone can edit their cashout account
+ // If 'false' only the admin
+ allow_edit_cashout_payto_uri?: boolean;
+
+ // Default debt limit for newly created accounts
+ default_debit_threshold?: AmountString;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // TAN channels supported by the server
+ supported_tan_channels?: TanChannel[];
+
+ // Wire transfer type supported by the bank.
+ // Default to 'iban' is missing
+ // @since v4, may become mandatory in the future.
+ wire_type?: string;
+
+ // Wire transfer execution fees.
+ // @since v4, will become mandatory in the next version.
+ wire_transfer_fees?: AmountString;
+}
+
+export interface BankAccountCreateWithdrawalRequest {
+ // Amount to withdraw. If given, the wallet
+ // cannot change the amount.
+ // Optional since **vC2EC**.
+ amount?: AmountString;
+
+ // Suggested amount to withdraw. The wallet can
+ // still change the suggestion.
+ // @since **vC2EC**
+ suggested_amount?: AmountString;
+}
+
+export interface BankAccountCreateWithdrawalResponse {
+ // ID of the withdrawal, can be used to view/modify the withdrawal operation.
+ withdrawal_id: string;
+
+ // URI that can be passed to the wallet to initiate the withdrawal.
+ taler_withdraw_uri: TalerUriString;
+}
+export interface WithdrawalPublicInfo {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount?: AmountString;
+
+ // Suggestion for the amount to be withdrawn with this
+ // operation. Given if a suggestion was made but the
+ // user may still change the amount.
+ // Optional since **vC2EC**.
+ suggested_amount?: AmountString;
+
+ // Account username
+ username: string;
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet
+ // only non-null if status is selected or confirmed.
+ selected_exchange_account?: PaytoString;
+}
+
+export interface BankAccountTransactionsResponse {
+ transactions: BankAccountTransactionInfo[];
+}
+
+export interface BankAccountTransactionInfo {
+ creditor_payto_uri: PaytoString;
+ debtor_payto_uri: PaytoString;
+
+ amount: AmountString;
+ direction: "debit" | "credit";
+
+ subject: string;
+
+ // Transaction unique ID. Matches
+ // $transaction_id from the URI.
+ row_id: number;
+ date: Timestamp;
+}
+
+export interface CreateTransactionRequest {
+ // Address in the Payto format of the wire transfer receiver.
+ // It needs at least the 'message' query string parameter.
+ payto_uri: PaytoString;
+
+ // Transaction amount (in the $currency:x.y format), optional.
+ // However, when not given, its value must occupy the 'amount'
+ // query string parameter of the 'payto' field. In case it
+ // is given in both places, the paytoUri's takes the precedence.
+ amount?: AmountString;
+
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ // @since v4, will become mandatory in the next version.
+ request_uid?: ShortHashCode;
+}
+
+export interface CreateTransactionResponse {
+ // ID identifying the transaction being created
+ row_id: Integer;
+}
+
+export interface RegisterAccountResponse {
+ // Internal payto URI of this bank account.
+ internal_payto_uri: PaytoString;
+}
+
+export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ cashout_payto_uri?: PaytoString;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: PaytoString;
+
+ // If present, set the max debit allowed for this user
+ // Only admin can set this property.
+ debit_threshold?: AmountString;
+
+ // If present, set a custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
+ // If present, enables 2FA and set the TAN channel used for challenges
+ // Only admin can set this property, other user can reconfig their account
+ // after creation.
+ tan_channel?: TanChannel;
+}
+
+export type EmailAddress = string;
+export type PhoneNumber = string;
+
+export interface ChallengeContactData {
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+}
+
+export interface AccountReconfiguration {
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ // Only admin can change this property.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' URI of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ // Only admin can change this property if not allowed in config
+ cashout_payto_uri?: PaytoString;
+
+ // If present, change the legal name associated with $username.
+ // Only admin can change this property if not allowed in config
+ name?: string;
+
+ // Make this account visible to anyone?
+ is_public?: boolean;
+
+ // If present, change the max debit allowed for this user
+ // Only admin can change this property.
+ debit_threshold?: AmountString;
+
+ // If present, change the custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
+ // If present, enables 2FA and set the TAN channel used for challenges
+ tan_channel?: TanChannel | null;
+}
+
+export interface AccountPasswordChange {
+ // New password.
+ new_password: string;
+ // Old password. If present, check that the old password matches.
+ // Optional for admin account.
+ old_password?: string;
+}
+
+export interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+}
+export interface PublicAccount {
+ // Username of the account
+ username: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: string;
+
+ // Current balance of the account
+ balance: Balance;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+}
+
+export interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+}
+export interface Balance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+}
+export interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: PaytoString;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
+}
+
+export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: PaytoString;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: PaytoString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Is 2FA enabled and what channel is used for challenges?
+ tan_channel?: TanChannel;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
+}
+
+export interface CashoutRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: ShortHashCode;
+
+ // Optional subject to associate to the
+ // cashout operation. This data will appear
+ // as the incoming wire transfer subject in
+ // the user's fiat bank account.
+ subject?: string;
+
+ // That is the plain amount that the user specified
+ // to cashout. Its $currency is the (regional) currency of the
+ // bank instance.
+ amount_debit: AmountString;
+
+ // That is the amount that will effectively be
+ // transferred by the bank to the user's bank
+ // account, that is external to the regional currency.
+ // It is expressed in the fiat currency and
+ // is calculated after the cashout fee and the
+ // exchange rate. See the /cashout-rates call.
+ // The client needs to calculate this amount
+ // correctly based on the amount_debit and the cashout rate,
+ // otherwise the request will fail.
+ amount_credit: AmountString;
+}
+
+export interface CashoutResponse {
+ // ID identifying the operation being created
+ cashout_id: number;
+}
+
+/**
+ * @deprecated since 4, use 2fa
+ */
+export interface CashoutConfirmRequest {
+ // the TAN that confirms $CASHOUT_ID.
+ tan: string;
+}
+
+export interface Cashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: CashoutInfo[];
+}
+
+export interface CashoutInfo {
+ cashout_id: number;
+}
+export interface GlobalCashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: GlobalCashoutInfo[];
+}
+export interface GlobalCashoutInfo {
+ cashout_id: number;
+ username: string;
+}
+
+export interface CashoutStatusResponse {
+ // Amount debited to the internal
+ // regional currency bank account.
+ amount_debit: AmountString;
+
+ // Amount credited to the external bank account.
+ amount_credit: AmountString;
+
+ // Transaction subject.
+ subject: string;
+
+ // Time when the cashout was created.
+ creation_time: Timestamp;
+}
+
+export interface ConversionRatesResponse {
+ // Exchange rate to buy the local currency from the external one
+ buy_at_ratio: DecimalNumber;
+
+ // Exchange rate to sell the local currency for the external one
+ sell_at_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the buy ratio.
+ buy_in_fee: DecimalNumber;
+
+ // Fee to subtract after applying the sell ratio.
+ sell_out_fee: DecimalNumber;
+}
+
+export enum MonitorTimeframeParam {
+ hour,
+ day,
+ month,
+ year,
+ decade,
+}
+
+export type MonitorResponse = MonitorNoConversion | MonitorWithConversion;
+
+// Monitoring stats when conversion is not supported
+export interface MonitorNoConversion {
+ type: "no-conversions";
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+}
+// Monitoring stats when conversion is supported
+export interface MonitorWithConversion {
+ type: "with-conversions";
+
+ // How many cashin operations were confirmed by a
+ // wallet owner. Note: wallet owners
+ // are NOT required to be customers of the libeufin-bank.
+ cashinCount: number;
+
+ // Overall regional currency that has been paid by the regional admin account
+ // to regional bank accounts to fulfill all the confirmed cashin operations.
+ cashinRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid to the fiat admin account
+ // by fiat bank accounts to fulfill all the confirmed cashin operations.
+ cashinFiatVolume: AmountString;
+
+ // How many cashout operations were confirmed.
+ cashoutCount: number;
+
+ // Overall regional currency that has been paid to the regional admin account
+ // by fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid by the fiat admin account
+ // to fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutFiatVolume: AmountString;
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+}
+export interface TanTransmission {
+ // Channel of the last successful transmission of the TAN challenge.
+ tan_channel: TanChannel;
+
+ // Info of the last successful transmission of the TAN challenge.
+ tan_info: string;
+}
+
+export interface Challenge {
+ // Unique identifier of the challenge to solve to run this protected
+ // operation.
+ challenge_id: number;
+}
+
+export interface ChallengeSolve {
+ // The TAN code that solves $CHALLENGE_ID
+ tan: string;
+}
+
+export enum TanChannel {
+ SMS = "sms",
+ EMAIL = "email",
+}
+
+export const codecForIntegrationBankConfig = (): Codec<IntegrationConfig> =>
+ buildCodecForObject<IntegrationConfig>()
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .build("TalerCorebankApi.IntegrationConfig");
+
+export const codecForCoreBankConfig = (): Codec<TalerCorebankConfigResponse> =>
+ buildCodecForObject<TalerCorebankConfigResponse>()
+ .property(
+ "name",
+ codecForEither(
+ codecForConstString("taler-corebank"),
+ codecForConstString("libeufin-bank"),
+ ),
+ )
+ .property("version", codecForString())
+ .property("bank_name", codecOptional(codecForString()))
+ .property("base_url", codecOptional(codecForString()))
+ .property("allow_conversion", codecOptional(codecForBoolean()))
+ .property("allow_registrations", codecOptional(codecForBoolean()))
+ .property("allow_deletions", codecOptional(codecForBoolean()))
+ .property("allow_edit_name", codecOptional(codecForBoolean()))
+ .property("allow_edit_cashout_payto_uri", codecOptional(codecForBoolean()))
+ .property("default_debit_threshold", codecOptional(codecForAmountString()))
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "supported_tan_channels",
+ codecOptional(
+ codecForList(
+ codecForEither(
+ codecForConstString(TanChannel.SMS),
+ codecForConstString(TanChannel.EMAIL),
+ ),
+ ),
+ ),
+ )
+ .property("wire_type", codecOptionalDefault(codecForString(), "iban"))
+ .property("wire_transfer_fees", codecOptional(codecForAmountString()))
+ .build("TalerCorebankApi.Config");
+
+const codecForBalance = (): Codec<Balance> =>
+ buildCodecForObject<Balance>()
+ .property("amount", codecForAmountString())
+ .property(
+ "credit_debit_indicator",
+ codecForEither(
+ codecForConstString("credit"),
+ codecForConstString("debit"),
+ ),
+ )
+ .build("TalerCorebankApi.Balance");
+
+const codecForPublicAccount = (): Codec<PublicAccount> =>
+ buildCodecForObject<PublicAccount>()
+ .property("username", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("is_taler_exchange", codecForBoolean())
+ .property("row_id", codecOptional(codecForNumber()))
+ .build("TalerCorebankApi.PublicAccount");
+
+export const codecForPublicAccountsResponse =
+ (): Codec<PublicAccountsResponse> =>
+ buildCodecForObject<PublicAccountsResponse>()
+ .property("public_accounts", codecForList(codecForPublicAccount()))
+ .build("TalerCorebankApi.PublicAccountsResponse");
+
+export const codecForAccountMinimalData = (): Codec<AccountMinimalData> =>
+ buildCodecForObject<AccountMinimalData>()
+ .property("username", codecForString())
+ .property("name", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("balance", codecForBalance())
+ .property("row_id", codecForNumber())
+ .property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
+ .build("TalerCorebankApi.AccountMinimalData");
+
+export const codecForListBankAccountsResponse =
+ (): Codec<ListBankAccountsResponse> =>
+ buildCodecForObject<ListBankAccountsResponse>()
+ .property("accounts", codecForList(codecForAccountMinimalData()))
+ .build("TalerCorebankApi.ListBankAccountsResponse");
+
+export const codecForAccountData = (): Codec<AccountData> =>
+ buildCodecForObject<AccountData>()
+ .property("name", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
+ .property("contact_data", codecOptional(codecForChallengeContactData()))
+ .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property(
+ "tan_channel",
+ codecOptional(
+ codecForEither(
+ codecForConstString(TanChannel.SMS),
+ codecForConstString(TanChannel.EMAIL),
+ ),
+ ),
+ )
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
+ .build("TalerCorebankApi.AccountData");
+
+export const codecForChallengeContactData = (): Codec<ChallengeContactData> =>
+ buildCodecForObject<ChallengeContactData>()
+ .property("email", codecOptional(codecForString()))
+ .property("phone", codecOptional(codecForString()))
+ .build("TalerCorebankApi.ChallengeContactData");
+
+export const codecForWithdrawalPublicInfo = (): Codec<WithdrawalPublicInfo> =>
+ buildCodecForObject<WithdrawalPublicInfo>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("username", codecForString())
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property("selected_exchange_account", codecOptional(codecForPaytoString()))
+ .build("TalerCorebankApi.WithdrawalPublicInfo");
+
+export const codecForBankAccountTransactionsResponse =
+ (): Codec<BankAccountTransactionsResponse> =>
+ buildCodecForObject<BankAccountTransactionsResponse>()
+ .property(
+ "transactions",
+ codecForList(codecForBankAccountTransactionInfo()),
+ )
+ .build("TalerCorebankApi.BankAccountTransactionsResponse");
+
+export const codecForBankAccountTransactionInfo =
+ (): Codec<BankAccountTransactionInfo> =>
+ buildCodecForObject<BankAccountTransactionInfo>()
+ .property("creditor_payto_uri", codecForPaytoString())
+ .property("debtor_payto_uri", codecForPaytoString())
+ .property("amount", codecForAmountString())
+ .property(
+ "direction",
+ codecForEither(
+ codecForConstString("debit"),
+ codecForConstString("credit"),
+ ),
+ )
+ .property("subject", codecForString())
+ .property("row_id", codecForNumber())
+ .property("date", codecForTimestamp)
+ .build("TalerCorebankApi.BankAccountTransactionInfo");
+
+export const codecForCreateTransactionResponse =
+ (): Codec<CreateTransactionResponse> =>
+ buildCodecForObject<CreateTransactionResponse>()
+ .property("row_id", codecForNumber())
+ .build("TalerCorebankApi.CreateTransactionResponse");
+
+export const codecForRegisterAccountResponse =
+ (): Codec<RegisterAccountResponse> =>
+ buildCodecForObject<RegisterAccountResponse>()
+ .property("internal_payto_uri", codecForPaytoString())
+ .build("TalerCorebankApi.RegisterAccountResponse");
+
+export const codecForBankAccountCreateWithdrawalResponse =
+ (): Codec<BankAccountCreateWithdrawalResponse> =>
+ buildCodecForObject<BankAccountCreateWithdrawalResponse>()
+ .property("taler_withdraw_uri", codecForTalerUriString())
+ .property("withdrawal_id", codecForString())
+ .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
+
+export const codecForCashoutPending = (): Codec<CashoutResponse> =>
+ buildCodecForObject<CashoutResponse>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutPending");
+
+export const codecForCashouts = (): Codec<Cashouts> =>
+ buildCodecForObject<Cashouts>()
+ .property("cashouts", codecForList(codecForCashoutInfo()))
+ .build("TalerCorebankApi.Cashouts");
+
+export const codecForCashoutInfo = (): Codec<CashoutInfo> =>
+ buildCodecForObject<CashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutInfo");
+
+export const codecForGlobalCashouts = (): Codec<GlobalCashouts> =>
+ buildCodecForObject<GlobalCashouts>()
+ .property("cashouts", codecForList(codecForGlobalCashoutInfo()))
+ .build("TalerCorebankApi.GlobalCashouts");
+
+export const codecForGlobalCashoutInfo = (): Codec<GlobalCashoutInfo> =>
+ buildCodecForObject<GlobalCashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .property("username", codecForString())
+ .build("TalerCorebankApi.GlobalCashoutInfo");
+
+export const codecForCashoutStatusResponse = (): Codec<CashoutStatusResponse> =>
+ buildCodecForObject<CashoutStatusResponse>()
+ .property("amount_debit", codecForAmountString())
+ .property("amount_credit", codecForAmountString())
+ .property("subject", codecForString())
+ .property("creation_time", codecForTimestamp)
+ .build("TalerCorebankApi.CashoutStatusResponse");
+
+export const codecForConversionRatesResponse =
+ (): Codec<ConversionRatesResponse> =>
+ buildCodecForObject<ConversionRatesResponse>()
+ .property("buy_at_ratio", codecForDecimalNumber())
+ .property("buy_in_fee", codecForDecimalNumber())
+ .property("sell_at_ratio", codecForDecimalNumber())
+ .property("sell_out_fee", codecForDecimalNumber())
+ .build("TalerCorebankApi.ConversionRatesResponse");
+
+export const codecForMonitorResponse = (): Codec<MonitorResponse> =>
+ buildCodecForUnion<MonitorResponse>()
+ .discriminateOn("type")
+ .alternative("no-conversions", codecForMonitorNoConversion())
+ .alternative("with-conversions", codecForMonitorWithCashout())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForMonitorNoConversion = (): Codec<MonitorNoConversion> =>
+ buildCodecForObject<MonitorNoConversion>()
+ .property("type", codecForConstString("no-conversions"))
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorJustPayouts");
+
+export const codecForMonitorWithCashout = (): Codec<MonitorWithConversion> =>
+ buildCodecForObject<MonitorWithConversion>()
+ .property("type", codecForConstString("with-conversions"))
+ .property("cashinCount", codecForNumber())
+ .property("cashinFiatVolume", codecForAmountString())
+ .property("cashinRegionalVolume", codecForAmountString())
+ .property("cashoutCount", codecForNumber())
+ .property("cashoutFiatVolume", codecForAmountString())
+ .property("cashoutRegionalVolume", codecForAmountString())
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorWithCashout");
+
+export const codecForChallenge = (): Codec<Challenge> =>
+ buildCodecForObject<Challenge>()
+ .property("challenge_id", codecForNumber())
+ .build("TalerCorebankApi.Challenge");
+
+export const codecForTanTransmission = (): Codec<TanTransmission> =>
+ buildCodecForObject<TanTransmission>()
+ .property(
+ "tan_channel",
+ codecForEither(
+ codecForConstString(TanChannel.SMS),
+ codecForConstString(TanChannel.EMAIL),
+ ),
+ )
+ .property("tan_info", codecForString())
+ .build("TalerCorebankApi.TanTransmission");
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
new file mode 100644
index 000000000..7c879b52e
--- /dev/null
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -0,0 +1,2926 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForList,
+ codecForNumber,
+ codecForString,
+ codecForStringURL,
+ codecOptional,
+} from "./codec.js";
+import {
+ PaytoString,
+ buildCodecForUnion,
+ codecForAmountString,
+ codecForBoolean,
+ codecForConstString,
+ codecForCurrencySpecificiation,
+ codecForEither,
+ codecForMap,
+ codecForURN,
+ codecOptionalDefault,
+ strcmp,
+} from "./index.js";
+import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
+import {
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
+} from "./time.js";
+import {
+ AccessToken,
+ AmlOfficerPublicKeyP,
+ AmountString,
+ Base32String,
+ CoinPublicKeyString,
+ Cs25519Point,
+ CurrencySpecification,
+ EddsaPublicKeyString,
+ EddsaSignatureString,
+ HashCodeString,
+ Integer,
+ InternationalizedString,
+ LibtoolVersionString,
+ PaytoHash,
+ RelativeTime,
+ RsaPublicKey,
+ RsaPublicKeySring,
+ Timestamp,
+ WireSalt,
+ codecForAccessToken,
+ codecForInternationalizedString,
+ codecForURLString,
+} from "./types-taler-common.js";
+
+export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
+
+export interface RsaDenominationPubKey {
+ readonly cipher: DenomKeyType.Rsa;
+ readonly rsa_public_key: string;
+ readonly age_mask: number;
+}
+
+export interface CsDenominationPubKey {
+ readonly cipher: DenomKeyType.ClauseSchnorr;
+ readonly age_mask: number;
+ readonly cs_public_key: string;
+}
+
+export namespace DenominationPubKey {
+ export function cmp(
+ p1: DenominationPubKey,
+ p2: DenominationPubKey,
+ ): -1 | 0 | 1 {
+ if (p1.cipher < p2.cipher) {
+ return -1;
+ } else if (p1.cipher > p2.cipher) {
+ return +1;
+ } else if (
+ p1.cipher === DenomKeyType.Rsa &&
+ p2.cipher === DenomKeyType.Rsa
+ ) {
+ if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
+ return -1;
+ } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
+ return 1;
+ }
+ return strcmp(p1.rsa_public_key, p2.rsa_public_key);
+ } else if (
+ p1.cipher === DenomKeyType.ClauseSchnorr &&
+ p2.cipher === DenomKeyType.ClauseSchnorr
+ ) {
+ if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
+ return -1;
+ } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
+ return 1;
+ }
+ return strcmp(p1.cs_public_key, p2.cs_public_key);
+ } else {
+ throw Error("unsupported cipher");
+ }
+ }
+}
+
+export const codecForRsaDenominationPubKey = () =>
+ buildCodecForObject<RsaDenominationPubKey>()
+ .property("cipher", codecForConstString(DenomKeyType.Rsa))
+ .property("rsa_public_key", codecForString())
+ .property("age_mask", codecForNumber())
+ .build("DenominationPubKey");
+
+export const codecForCsDenominationPubKey = () =>
+ buildCodecForObject<CsDenominationPubKey>()
+ .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
+ .property("cs_public_key", codecForString())
+ .property("age_mask", codecForNumber())
+ .build("CsDenominationPubKey");
+
+export const codecForDenominationPubKey = () =>
+ buildCodecForUnion<DenominationPubKey>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
+ .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
+ .build("DenominationPubKey");
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+export interface ExchangeDenomination {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: string;
+
+ /**
+ * Public signing key of the denomination.
+ */
+ denom_pub: DenominationPubKey;
+
+ /**
+ * Fee for withdrawing.
+ */
+ fee_withdraw: string;
+
+ /**
+ * Fee for depositing.
+ */
+ fee_deposit: string;
+
+ /**
+ * Fee for refreshing.
+ */
+ fee_refresh: string;
+
+ /**
+ * Fee for refunding.
+ */
+ fee_refund: string;
+
+ /**
+ * Start date from which withdraw is allowed.
+ */
+ stamp_start: TalerProtocolTimestamp;
+
+ /**
+ * End date for withdrawing.
+ */
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ /**
+ * Expiration date after which the exchange can forget about
+ * the currency.
+ */
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ /**
+ * Date after which the coins of this denomination can't be
+ * deposited anymore.
+ */
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ /**
+ * Signature over the denomination information by the exchange's master
+ * signing key.
+ */
+ master_sig: string;
+}
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+export interface AuditorDenomSig {
+ /**
+ * Denomination public key's hash.
+ */
+ denom_pub_h: string;
+
+ /**
+ * The signature.
+ */
+ auditor_sig: string;
+}
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+export interface ExchangeAuditor {
+ /**
+ * Auditor's public key.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ auditor_url: string;
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ denomination_keys: AuditorDenomSig[];
+}
+
+export type ExchangeWithdrawValue =
+ | ExchangeRsaWithdrawValue
+ | ExchangeCsWithdrawValue;
+
+export interface ExchangeRsaWithdrawValue {
+ cipher: "RSA";
+}
+
+export interface ExchangeCsWithdrawValue {
+ cipher: "CS";
+
+ /**
+ * CSR R0 value
+ */
+ r_pub_0: string;
+
+ /**
+ * CSR R1 value
+ */
+ r_pub_1: string;
+}
+
+export interface RecoupRequest {
+ /**
+ * Hashed denomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ *
+ * The string variant is for the legacy exchange protocol.
+ */
+ denom_sig: UnblindedSignature;
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature of TALER_RecoupRequestPS created with the coin's private key.
+ */
+ coin_sig: string;
+
+ ewv: ExchangeWithdrawValue;
+}
+
+export interface RecoupRefreshRequest {
+ /**
+ * Hashed enomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ *
+ * The string variant is for the legacy exchange protocol.
+ */
+ denom_sig: UnblindedSignature;
+
+ /**
+ * Coin's blinding factor.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature of TALER_RecoupRefreshRequestPS created with
+ * the coin's private key.
+ */
+ coin_sig: string;
+
+ ewv: ExchangeWithdrawValue;
+}
+
+/**
+ * Response that we get from the exchange for a payback request.
+ */
+export interface RecoupConfirmation {
+ /**
+ * Public key of the reserve that will receive the payback.
+ */
+ reserve_pub?: string;
+
+ /**
+ * Public key of the old coin that will receive the recoup,
+ * provided if refreshed was true.
+ */
+ old_coin_pub?: string;
+}
+
+export type UnblindedSignature = RsaUnblindedSignature;
+
+export interface RsaUnblindedSignature {
+ cipher: DenomKeyType.Rsa;
+ rsa_signature: string;
+}
+
+/**
+ * Deposit permission for a single coin.
+ */
+export interface CoinDepositPermission {
+ /**
+ * Signature by the coin.
+ */
+ coin_sig: string;
+
+ /**
+ * Public key of the coin being spend.
+ */
+ coin_pub: string;
+
+ /**
+ * Signature made by the denomination public key.
+ *
+ * The string variant is for legacy protocol support.
+ */
+
+ ub_sig: UnblindedSignature;
+
+ /**
+ * The denomination public key associated with this coin.
+ */
+ h_denom: string;
+
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ contribution: string;
+
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ exchange_url: string;
+
+ minimum_age_sig?: EddsaSignatureString;
+
+ age_commitment?: Edx25519PublicKeyEnc[];
+
+ h_age_commitment?: string;
+}
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+export interface Recoup {
+ /**
+ * The hash of the denomination public key for which the payback is offered.
+ */
+ h_denom_pub: string;
+}
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+export interface ExchangeKeysJson {
+ /**
+ * Canonical, public base URL of the exchange.
+ */
+ base_url: string;
+
+ currency: string;
+
+ currency_specification?: CurrencySpecification;
+
+ /**
+ * The exchange's master public key.
+ */
+ master_public_key: string;
+
+ /**
+ * The list of auditors (partially) auditing the exchange.
+ */
+ auditors: ExchangeAuditor[];
+
+ /**
+ * Timestamp when this response was issued.
+ */
+ list_issue_date: TalerProtocolTimestamp;
+
+ /**
+ * List of revoked denominations.
+ */
+ recoup?: Recoup[];
+
+ /**
+ * Short-lived signing keys used to sign online
+ * responses.
+ */
+ signkeys: ExchangeSignKeyJson[];
+
+ /**
+ * Protocol version.
+ */
+ version: string;
+
+ reserve_closing_delay: TalerProtocolDuration;
+
+ global_fees: GlobalFees[];
+
+ accounts: ExchangeWireAccount[];
+
+ wire_fees: { [methodName: string]: WireFeesJson[] };
+
+ denominations: DenomGroup[];
+
+ // Threshold amounts beyond which wallet should
+ // trigger the KYC process of the issuing exchange.
+ // Optional option, if not given there is no limit.
+ // Currency must match currency.
+ wallet_balance_limit_without_kyc?: AmountString[];
+
+ // Array of limits that apply to all accounts.
+ // All of the given limits will be hard limits.
+ // Wallets and merchants are expected to obey them
+ // and not even allow the user to cross them.
+ // Since protocol **v21**.
+ hard_limits?: AccountLimit[];
+
+ // Array of limits with a soft threshold of zero
+ // that apply to all accounts without KYC.
+ // Wallets and merchants are expected to trigger
+ // a KYC process before attempting any zero-limited
+ // operations.
+ // Since protocol **v21**.
+ zero_limits?: ZeroLimitedOperation[];
+}
+
+export interface ExchangeMeltRequest {
+ coin_pub: CoinPublicKeyString;
+ confirm_sig: EddsaSignatureString;
+ denom_pub_hash: HashCodeString;
+ denom_sig: UnblindedSignature;
+ rc: string;
+ value_with_fee: AmountString;
+ age_commitment_hash?: HashCodeString;
+}
+
+export interface ExchangeMeltResponse {
+ /**
+ * Which of the kappa indices does the client not have to reveal.
+ */
+ noreveal_index: number;
+
+ /**
+ * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+ * affirms the successful melt and confirming the noreveal_index
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /*
+ * public EdDSA key of the exchange that was used to generate the signature.
+ * Should match one of the exchange's signing keys from /keys. Again given
+ * explicitly as the client might otherwise be confused by clock skew as to
+ * which signing key was used.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /*
+ * Base URL to use for operations on the refresh context
+ * (so the reveal operation). If not given,
+ * the base URL is the same as the one used for this request.
+ * Can be used if the base URL for /refreshes/ differs from that
+ * for /coins/, i.e. for load balancing. Clients SHOULD
+ * respect the refresh_base_url if provided. Any HTTP server
+ * belonging to an exchange MUST generate a 307 or 308 redirection
+ * to the correct base URL should a client uses the wrong base
+ * URL, or if the base URL has changed since the melt.
+ *
+ * When melting the same coin twice (technically allowed
+ * as the response might have been lost on the network),
+ * the exchange may return different values for the refresh_base_url.
+ */
+ refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+ ev_sig: BlindedDenominationSignature;
+}
+
+export interface ExchangeRevealResponse {
+ // List of the exchange's blinded RSA signatures on the new coins.
+ ev_sigs: ExchangeRevealItem[];
+}
+
+export const codecForDenomination = (): Codec<ExchangeDenomination> =>
+ buildCodecForObject<ExchangeDenomination>()
+ .property("value", codecForString())
+ .property("denom_pub", codecForDenominationPubKey())
+ .property("fee_withdraw", codecForString())
+ .property("fee_deposit", codecForString())
+ .property("fee_refresh", codecForString())
+ .property("fee_refund", codecForString())
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire_withdraw", codecForTimestamp)
+ .property("stamp_expire_legal", codecForTimestamp)
+ .property("stamp_expire_deposit", codecForTimestamp)
+ .property("master_sig", codecForString())
+ .build("Denomination");
+
+export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ buildCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString())
+ .property("auditor_sig", codecForString())
+ .build("AuditorDenomSig");
+
+export const codecForAuditor = (): Codec<ExchangeAuditor> =>
+ buildCodecForObject<ExchangeAuditor>()
+ .property("auditor_pub", codecForString())
+ .property("auditor_url", codecForString())
+ .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
+ .build("Auditor");
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class ExchangeSignKeyJson {
+ stamp_start: TalerProtocolTimestamp;
+ stamp_expire: TalerProtocolTimestamp;
+ stamp_end: TalerProtocolTimestamp;
+ key: EddsaPublicKeyString;
+ master_sig: EddsaSignatureString;
+}
+
+export type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+
+export interface DenomGroupCommon {
+ // How much are coins of this denomination worth?
+ value: AmountString;
+
+ // Fee charged by the exchange for withdrawing a coin of this denomination.
+ fee_withdraw: AmountString;
+
+ // Fee charged by the exchange for depositing a coin of this denomination.
+ fee_deposit: AmountString;
+
+ // Fee charged by the exchange for refreshing a coin of this denomination.
+ fee_refresh: AmountString;
+
+ // Fee charged by the exchange for refunding a coin of this denomination.
+ fee_refund: AmountString;
+}
+
+export interface DenomCommon {
+ // Signature of TALER_DenominationKeyValidityPS.
+ master_sig: EddsaSignatureString;
+
+ // When does the denomination key become valid?
+ stamp_start: TalerProtocolTimestamp;
+
+ // When is it no longer possible to deposit coins
+ // of this denomination?
+ stamp_expire_withdraw: TalerProtocolTimestamp;
+
+ // Timestamp indicating by when legal disputes relating to these coins must
+ // be settled, as the exchange will afterwards destroy its evidence relating to
+ // transactions involving this coin.
+ stamp_expire_legal: TalerProtocolTimestamp;
+
+ stamp_expire_deposit: TalerProtocolTimestamp;
+
+ // Set to 'true' if the exchange somehow "lost"
+ // the private key. The denomination was not
+ // necessarily revoked, but still cannot be used
+ // to withdraw coins at this time (theoretically,
+ // the private key could be recovered in the
+ // future; coins signed with the private key
+ // remain valid).
+ lost?: boolean;
+}
+
+export interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+}
+
+/**
+ * Wire fees as announced by the exchange.
+ */
+export class WireFeesJson {
+ /**
+ * Cost of a wire transfer.
+ */
+ wire_fee: string;
+
+ /**
+ * Cost of clising a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Signature made with the exchange's master key.
+ */
+ sig: string;
+
+ /**
+ * Date from which the fee applies.
+ */
+ start_date: TalerProtocolTimestamp;
+
+ /**
+ * Data after which the fee doesn't apply anymore.
+ */
+ end_date: TalerProtocolTimestamp;
+}
+
+export interface ExchangeWireAccount {
+ // 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: EddsaSignatureString;
+
+ // Display label wallets should use to show this
+ // bank account.
+ // Since protocol **v19**.
+ bank_label?: string;
+ priority?: number;
+}
+
+export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> =>
+ buildCodecForObject<ExchangeWireAccount>()
+ .property("conversion_url", codecOptional(codecForStringURL()))
+ .property("credit_restrictions", codecForList(codecForAny()))
+ .property("debit_restrictions", codecForList(codecForAny()))
+ .property("master_sig", codecForString())
+ .property("payto_uri", codecForString())
+ .property("bank_label", codecOptional(codecForString()))
+ .property("priority", codecOptional(codecForNumber()))
+ .build("WireAccount");
+
+export interface ExchangeRefundRequest {
+ // Amount to be refunded, can be a fraction of the
+ // coin's total deposit value (including deposit fee);
+ // must be larger than the refund fee.
+ refund_amount: AmountString;
+
+ // SHA-512 hash of the contact of the merchant with the customer.
+ h_contract_terms: HashCodeString;
+
+ // 64-bit transaction id of the refund transaction between merchant and customer.
+ rtransaction_id: number;
+
+ // EdDSA public key of the merchant.
+ merchant_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the merchant over a
+ // TALER_RefundRequestPS with purpose
+ // TALER_SIGNATURE_MERCHANT_REFUND
+ // affirming the refund.
+ merchant_sig: EddsaPublicKeyString;
+}
+
+export interface ExchangeRefundSuccessResponse {
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
+ // a TALER_RecoupRefreshConfirmationPS
+ // using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForExchangeRefundSuccessResponse =
+ (): Codec<ExchangeRefundSuccessResponse> =>
+ buildCodecForObject<ExchangeRefundSuccessResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .build("ExchangeRefundSuccessResponse");
+
+export type AccountRestriction =
+ | RegexAccountRestriction
+ | DenyAllAccountRestriction;
+
+export interface DenyAllAccountRestriction {
+ type: "deny";
+}
+
+// Accounts interacting with this type of account
+// restriction must have a payto://-URI matching
+// the given regex.
+export 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?: InternationalizedString;
+}
+
+export type CoinEnvelope = CoinEnvelopeRsa | CoinEnvelopeCs;
+
+export interface CoinEnvelopeRsa {
+ cipher: DenomKeyType.Rsa;
+ rsa_blinded_planchet: string;
+}
+
+export interface CoinEnvelopeCs {
+ cipher: DenomKeyType.ClauseSchnorr;
+ // FIXME: add remaining fields
+}
+
+export interface ExchangeWithdrawRequest {
+ denom_pub_hash: HashCodeString;
+ reserve_sig: EddsaSignatureString;
+ coin_ev: CoinEnvelope;
+}
+
+export interface ExchangeBatchWithdrawRequest {
+ planchets: ExchangeWithdrawRequest[];
+}
+
+export interface ExchangeRefreshRevealRequest {
+ new_denoms_h: HashCodeString[];
+ coin_evs: CoinEnvelope[];
+ /**
+ * kappa - 1 transfer private keys (ephemeral ECDHE keys).
+ */
+ transfer_privs: string[];
+
+ transfer_pub: EddsaPublicKeyString;
+
+ link_sigs: EddsaSignatureString[];
+
+ /**
+ * Iff the corresponding denomination has support for age restriction,
+ * the client MUST provide the original age commitment, i.e. the vector
+ * of public keys.
+ */
+ old_age_commitment?: Edx25519PublicKeyEnc[];
+}
+
+export const codecForRecoup = (): Codec<Recoup> =>
+ buildCodecForObject<Recoup>()
+ .property("h_denom_pub", codecForString())
+ .build("Recoup");
+
+export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
+ buildCodecForObject<ExchangeSignKeyJson>()
+ .property("key", codecForString())
+ .property("master_sig", codecForString())
+ .property("stamp_end", codecForTimestamp)
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire", codecForTimestamp)
+ .build("ExchangeSignKeyJson");
+
+export const codecForGlobalFees = (): Codec<GlobalFees> =>
+ buildCodecForObject<GlobalFees>()
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .property("history_fee", codecForAmountString())
+ .property("account_fee", codecForAmountString())
+ .property("purse_fee", codecForAmountString())
+ .property("history_expiration", codecForDuration)
+ .property("purse_account_limit", codecForNumber())
+ .property("purse_timeout", codecForDuration)
+ .property("master_sig", codecForString())
+ .build("GlobalFees");
+
+// FIXME: Validate properly!
+export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();
+
+export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
+ buildCodecForObject<ExchangeKeysJson>()
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .property(
+ "currency_specification",
+ codecOptional(codecForCurrencySpecificiation()),
+ )
+ .property("master_public_key", codecForString())
+ .property("auditors", codecForList(codecForAuditor()))
+ .property("list_issue_date", codecForTimestamp)
+ .property("recoup", codecOptional(codecForList(codecForRecoup())))
+ .property("signkeys", codecForList(codecForExchangeSigningKey()))
+ .property("version", codecForString())
+ .property("reserve_closing_delay", codecForDuration)
+ .property("global_fees", codecForList(codecForGlobalFees()))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .property("zero_limits", codecOptional(codecForList(codecForZeroLimitedOperation())))
+ .property("hard_limits", codecOptional(codecForList(codecForAccountLimit())))
+ .property("denominations", codecForList(codecForNgDenominations))
+ .property(
+ "wallet_balance_limit_without_kyc",
+ codecOptional(codecForList(codecForAmountString())),
+ )
+ .build("ExchangeKeysJson");
+
+export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
+ buildCodecForObject<WireFeesJson>()
+ .property("wire_fee", codecForString())
+ .property("closing_fee", codecForString())
+ .property("sig", codecForString())
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .build("WireFeesJson");
+
+export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
+ buildCodecForObject<RecoupConfirmation>()
+ .property("reserve_pub", codecOptional(codecForString()))
+ .property("old_coin_pub", codecOptional(codecForString()))
+ .build("RecoupConfirmation");
+
+export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
+ buildCodecForObject<ExchangeWithdrawResponse>()
+ .property("ev_sig", codecForBlindedDenominationSignature())
+ .build("WithdrawResponse");
+
+export class ExchangeWithdrawResponse {
+ ev_sig: BlindedDenominationSignature;
+}
+
+export class ExchangeWithdrawBatchResponse {
+ ev_sigs: ExchangeWithdrawResponse[];
+}
+
+export enum DenomKeyType {
+ Rsa = "RSA",
+ ClauseSchnorr = "CS",
+}
+
+export namespace DenomKeyType {
+ export function toIntTag(t: DenomKeyType): number {
+ switch (t) {
+ case DenomKeyType.Rsa:
+ return 1;
+ case DenomKeyType.ClauseSchnorr:
+ return 2;
+ }
+ }
+}
+
+// export interface RsaBlindedDenominationSignature {
+// cipher: DenomKeyType.Rsa;
+// blinded_rsa_signature: string;
+// }
+
+// export interface CSBlindedDenominationSignature {
+// cipher: DenomKeyType.ClauseSchnorr;
+// }
+
+// export type BlindedDenominationSignature =
+// | RsaBlindedDenominationSignature
+// | CSBlindedDenominationSignature;
+
+export const codecForRsaBlindedDenominationSignature = () =>
+ buildCodecForObject<RsaBlindedDenominationSignature>()
+ .property("cipher", codecForConstString(DenomKeyType.Rsa))
+ .property("blinded_rsa_signature", codecForString())
+ .build("RsaBlindedDenominationSignature");
+
+export const codecForBlindedDenominationSignature = () =>
+ buildCodecForUnion<BlindedDenominationSignature>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
+ .build("BlindedDenominationSignature");
+
+export const codecForExchangeWithdrawBatchResponse =
+ (): Codec<ExchangeWithdrawBatchResponse> =>
+ buildCodecForObject<ExchangeWithdrawBatchResponse>()
+ .property("ev_sigs", codecForList(codecForWithdrawResponse()))
+ .build("WithdrawBatchResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+ buildCodecForObject<ExchangeMeltResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("noreveal_index", codecForNumber())
+ .property("refresh_base_url", codecOptional(codecForString()))
+ .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+ buildCodecForObject<ExchangeRevealItem>()
+ .property("ev_sig", codecForBlindedDenominationSignature())
+ .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse =
+ (): Codec<ExchangeRevealResponse> =>
+ buildCodecForObject<ExchangeRevealResponse>()
+ .property("ev_sigs", codecForList(codecForExchangeRevealItem()))
+ .build("ExchangeRevealResponse");
+
+export interface FutureKeysResponse {
+ future_denoms: any[];
+
+ future_signkeys: any[];
+
+ master_pub: string;
+
+ denom_secmod_public_key: string;
+
+ // Public key of the signkey security module.
+ signkey_secmod_public_key: string;
+}
+
+export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
+ buildCodecForObject<FutureKeysResponse>()
+ .property("master_pub", codecForString())
+ .property("future_signkeys", codecForList(codecForAny()))
+ .property("future_denoms", codecForList(codecForAny()))
+ .property("denom_secmod_public_key", codecForAny())
+ .property("signkey_secmod_public_key", codecForAny())
+ .build("FutureKeysResponse");
+
+export interface PurseDeposit {
+ /**
+ * Amount to be deposited, can be a fraction of the
+ * coin's total value.
+ */
+ amount: AmountString;
+
+ /**
+ * Hash of denomination RSA key with which the coin is signed.
+ */
+ denom_pub_hash: HashCodeString;
+
+ /**
+ * Exchange's unblinded RSA signature of the coin.
+ */
+ ub_sig: UnblindedSignature;
+
+ /**
+ * Age commitment for the coin, if the denomination is age-restricted.
+ */
+ age_commitment?: string[];
+
+ /**
+ * Attestation for the minimum age, if the denomination is age-restricted.
+ */
+ attest?: string;
+
+ /**
+ * Signature over TALER_PurseDepositSignaturePS
+ * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
+ * made by the customer with the
+ * coin's private key.
+ */
+ coin_sig: EddsaSignatureString;
+
+ /**
+ * Public key of the coin being deposited into the purse.
+ */
+ coin_pub: EddsaPublicKeyString;
+}
+
+export interface ExchangePurseMergeRequest {
+ // payto://-URI of the account the purse is to be merged into.
+ // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
+ payto_uri: string;
+
+ // EdDSA signature of the account/reserve affirming the merge
+ // over a TALER_AccountMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+}
+
+export interface ExchangeGetContractResponse {
+ purse_pub: string;
+ econtract_sig: string;
+ econtract: string;
+}
+
+export const codecForExchangeGetContractResponse =
+ (): Codec<ExchangeGetContractResponse> =>
+ buildCodecForObject<ExchangeGetContractResponse>()
+ .property("purse_pub", codecForString())
+ .property("econtract_sig", codecForString())
+ .property("econtract", codecForString())
+ .build("ExchangeGetContractResponse");
+
+/**
+ * Contract terms between two wallets (as opposed to a merchant and wallet).
+ */
+export interface PeerContractTerms {
+ amount: AmountString;
+ summary: string;
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface EncryptedContract {
+ // Encrypted contract.
+ econtract: string;
+
+ // Signature over the (encrypted) contract.
+ econtract_sig: string;
+
+ // Ephemeral public key for the DH operation to decrypt the encrypted contract.
+ contract_pub: string;
+}
+
+/**
+ * Payload for /reserves/{reserve_pub}/purse
+ * endpoint of the exchange.
+ */
+export interface ExchangeReservePurseRequest {
+ /**
+ * Minimum amount that must be credited to the reserve, that is
+ * the total value of the purse minus the deposit fees.
+ * If the deposit fees are lower, the contribution to the
+ * reserve can be higher!
+ */
+ purse_value: AmountString;
+
+ // Minimum age required for all coins deposited into the purse.
+ min_age: number;
+
+ // Purse fee the reserve owner is willing to pay
+ // for the purse creation. Optional, if not present
+ // the purse is to be created from the purse quota
+ // of the reserve.
+ purse_fee: AmountString;
+
+ // Optional encrypted contract, in case the buyer is
+ // proposing the contract and thus establishing the
+ // purse with the payment.
+ econtract?: EncryptedContract;
+
+ // EdDSA public key used to approve merges of this purse.
+ merge_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // EdDSA signature of the account/reserve affirming the merge.
+ // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // Purse public key.
+ purse_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse over
+ // TALER_PurseRequestSignaturePS of
+ // purpose TALER_SIGNATURE_PURSE_REQUEST
+ // confirming that the
+ // above details hold for this purse.
+ purse_sig: EddsaSignatureString;
+
+ // SHA-512 hash of the contact of the purse.
+ h_contract_terms: HashCodeString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the purse should expire
+ // if it has not been paid.
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface ExchangePurseDeposits {
+ // Array of coins to deposit into the purse.
+ deposits: PurseDeposit[];
+}
+
+/**
+ * @deprecated batch deposit should be used.
+ */
+export interface ExchangeDepositRequest {
+ // Amount to be deposited, can be a fraction of the
+ // coin's total value.
+ contribution: AmountString;
+
+ // The merchant's account details.
+ // In case of an auction policy, it refers to the seller.
+ merchant_payto_uri: string;
+
+ // The salt is used to hide the payto_uri from customers
+ // when computing the h_wire of the merchant.
+ wire_salt: string;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // Hash of denomination RSA key with which the coin is signed.
+ denom_pub_hash: HashCodeString;
+
+ // Exchange's unblinded RSA signature of the coin.
+ ub_sig: UnblindedSignature;
+
+ // Timestamp when the contract was finalized.
+ timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the exchange undertakes to transfer the funds to
+ // the merchant, in case of successful payment. A wire transfer deadline of 'never'
+ // is not allowed.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // EdDSA public key of the merchant, so that the client can identify the
+ // merchant for refund requests.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ merchant_pub: EddsaPublicKeyString;
+
+ // Date until which the merchant can issue a refund to the customer via the
+ // exchange, to be omitted if refunds are not allowed.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ refund_deadline?: TalerProtocolTimestamp;
+
+ // CAVEAT: THIS IS WORK IN PROGRESS
+ // (Optional) policy for the deposit.
+ // This might be a refund, auction or escrow policy.
+ //
+ // Note that support for policies is an optional feature of the exchange.
+ // Optional features are so called "extensions" in Taler. The exchange
+ // provides the list of supported extensions, including policies, in the
+ // ExtensionsManifestsResponse response to the /keys endpoint.
+ policy?: any;
+
+ // Signature over TALER_DepositRequestPS, made by the customer with the
+ // coin's private key.
+ coin_sig: EddsaSignatureString;
+
+ h_age_commitment?: string;
+}
+
+export type TrackTransaction =
+ | ({ type: "accepted" } & TrackTransactionAccepted)
+ | ({ type: "wired" } & TrackTransactionWired);
+
+export interface BatchDepositSuccess {
+ // Optional base URL of the exchange for looking up wire transfers
+ // associated with this transaction. If not given,
+ // the base URL is the same as the one used for this request.
+ // Can be used if the base URL for ``/transactions/`` differs from that
+ // for ``/coins/``, i.e. for load balancing. Clients SHOULD
+ // respect the ``transaction_base_url`` if provided. Any HTTP server
+ // belonging to an exchange MUST generate a 307 or 308 redirection
+ // to the correct base URL should a client uses the wrong base
+ // URL, or if the base URL has changed since the deposit.
+ transaction_base_url?: string;
+
+ // Timestamp when the deposit was received by the exchange.
+ exchange_timestamp: TalerProtocolTimestamp;
+
+ // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
+ // generate the signature.
+ // Should match one of the exchange's signing keys from ``/keys``. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Array of deposit confirmation signatures from the exchange
+ // Entries must be in the same order the coins were given
+ // in the batch deposit request.
+ exchange_sig: EddsaSignatureString;
+}
+
+export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
+ buildCodecForObject<BatchDepositSuccess>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_timestamp", codecForTimestamp)
+ .property("transaction_base_url", codecOptional(codecForString()))
+ .build("BatchDepositSuccess");
+
+export interface TrackTransactionWired {
+ // Raw wire transfer identifier of the deposit.
+ wtid: Base32String;
+
+ // When was the wire transfer given to the bank.
+ execution_time: TalerProtocolTimestamp;
+
+ // The contribution of this coin to the total (without fees)
+ coin_contribution: AmountString;
+
+ // Binary-only Signature_ with purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE
+ // over a TALER_ConfirmWirePS
+ // whereby the exchange affirms the successful wire transfer.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. Again given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> =>
+ buildCodecForObject<TrackTransactionWired>()
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("coin_contribution", codecForAmountString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .build("TackTransactionWired");
+
+export interface TrackTransactionAccepted {
+ // Legitimization target that the merchant should
+ // use to check for its KYC status using
+ // the /kyc-check/$REQUIREMENT_ROW/... endpoint.
+ // Optional, not present if the deposit has not
+ // yet been aggregated to the point that a KYC
+ // need has been evaluated.
+ requirement_row?: number;
+
+ // True if the KYC check for the merchant has been
+ // satisfied. False does not mean that KYC
+ // is strictly needed, unless also a
+ // legitimization_uuid is provided.
+ kyc_ok: boolean;
+
+ // Time by which the exchange currently thinks the deposit will be executed.
+ // Actual execution may be later if the KYC check is not satisfied by then.
+ execution_time: TalerProtocolTimestamp;
+}
+
+export const codecForTackTransactionAccepted =
+ (): Codec<TrackTransactionAccepted> =>
+ buildCodecForObject<TrackTransactionAccepted>()
+ .property("requirement_row", codecOptional(codecForNumber()))
+ .property("kyc_ok", codecForBoolean())
+ .property("execution_time", codecForTimestamp)
+ .build("TackTransactionAccepted");
+
+export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
+ buildCodecForObject<PeerContractTerms>()
+ .property("summary", codecForString())
+ .property("amount", codecForAmountString())
+ .property("purse_expiration", codecForTimestamp)
+ .build("PeerContractTerms");
+
+export interface ExchangeBatchDepositRequest {
+ // The merchant's account details.
+ merchant_payto_uri: string;
+
+ // The salt is used to hide the ``payto_uri`` from customers
+ // when computing the ``h_wire`` of the merchant.
+ wire_salt: WireSalt;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // The list of coins that are going to be deposited with this Request.
+ coins: BatchDepositRequestCoin[];
+
+ // Timestamp when the contract was finalized.
+ timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the exchange undertakes to transfer the funds to
+ // the merchant, in case of successful payment. A wire transfer deadline of 'never'
+ // is not allowed.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
+ // merchant for refund requests.
+ merchant_pub: EddsaPublicKeyString;
+
+ // Date until which the merchant can issue a refund to the customer via the
+ // exchange, to be omitted if refunds are not allowed.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ refund_deadline?: TalerProtocolTimestamp;
+
+ // CAVEAT: THIS IS WORK IN PROGRESS
+ // (Optional) policy for the batch-deposit.
+ // This might be a refund, auction or escrow policy.
+ policy?: any;
+}
+
+export interface BatchDepositRequestCoin {
+ // EdDSA public key of the coin being deposited.
+ coin_pub: EddsaPublicKeyString;
+
+ // Hash of denomination RSA key with which the coin is signed.
+ denom_pub_hash: HashCodeString;
+
+ // Exchange's unblinded RSA signature of the coin.
+ ub_sig: UnblindedSignature;
+
+ // Amount to be deposited, can be a fraction of the
+ // coin's total value.
+ contribution: AmountString;
+
+ // Signature over `TALER_DepositRequestPS`, made by the customer with the
+ // `coin's private key <coin-priv>`.
+ coin_sig: EddsaSignatureString;
+
+ h_age_commitment?: string;
+}
+
+export interface AvailableMeasureSummary {
+ // Available original measures that can be
+ // triggered directly by default rules.
+ roots: { [measure_name: string]: MeasureInformation };
+
+ // Available AML programs.
+ programs: { [prog_name: string]: AmlProgramRequirement };
+
+ // Available KYC checks.
+ checks: { [check_name: string]: KycCheckInformation };
+}
+
+export interface MeasureInformation {
+ // Name of a KYC check.
+ check_name: string;
+
+ // Name of an AML program.
+ prog_name: string;
+
+ // Context for the check. Optional.
+ context?: Object;
+
+ // Operation that this measure relates to.
+ // NULL if unknown. Useful as a hint to the
+ // user if there are many (voluntary) measures
+ // and some related to unlocking certain operations.
+ // (and due to zero-amount thresholds, no measure
+ // was actually specifically triggered).
+ //
+ // Must be one of "WITHDRAW", "DEPOSIT",
+ // (p2p) "MERGE", (wallet) "BALANCE",
+ // (reserve) "CLOSE", "AGGREGATE",
+ // "TRANSACTION" or "REFUND".
+ // New in protocol **v21**.
+ operation_type?: string;
+
+ // Can this measure be undertaken voluntarily?
+ // Optional, default is false.
+ // Since protocol **vATTEST**.
+ voluntary?: boolean;
+}
+
+export interface AmlProgramRequirement {
+ // Description of what the AML program does.
+ description: string;
+
+ // List of required field names in the context to run this
+ // AML program. SPA must check that the AML staff is providing
+ // adequate CONTEXT when defining a measure using this program.
+ context: string[];
+
+ // List of required attribute names in the
+ // input of this AML program. These attributes
+ // are the minimum that the check must produce
+ // (it may produce more).
+ inputs: string[];
+}
+
+export interface KycCheckInformation {
+ // Description of the KYC check. Should be shown
+ // to the AML staff but will also be shown to the
+ // client when they initiate the check in the KYC SPA.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // description texts.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Names of the fields that the CONTEXT must provide
+ // as inputs to this check.
+ // SPA must check that the AML staff is providing
+ // adequate CONTEXT when defining a measure using
+ // this check.
+ requires: string[];
+
+ // Names of the attributes the check will output.
+ // SPA must check that the outputs match the
+ // required inputs when combining a KYC check
+ // with an AML program into a measure.
+ outputs: string[];
+
+ // Name of a root measure taken when this check fails.
+ fallback: string;
+}
+
+export interface AmlDecisionDetails {
+ // Array of AML decisions made for this account. Possibly
+ // contains only the most recent decision if "history" was
+ // not set to 'true'.
+ aml_history: AmlDecisionDetail[];
+
+ // Array of KYC attributes obtained for this account.
+ kyc_attributes: KycDetail[];
+}
+
+export interface AmlDecisionDetail {
+ // What was the justification given?
+ justification: string;
+
+ // What is the new AML state.
+ new_state: Integer;
+
+ // When was this decision made?
+ decision_time: Timestamp;
+
+ // What is the new AML decision threshold (in monthly transaction volume)?
+ new_threshold: AmountString;
+
+ // Who made the decision?
+ decider_pub: AmlOfficerPublicKeyP;
+}
+
+export interface KycDetail {
+ // Name of the configuration section that specifies the provider
+ // which was used to collect the KYC details
+ provider_section: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+
+ // Time when the validity of the KYC data will expire
+ expiration_time: Timestamp;
+}
+
+export type AmlDecisionRequestWithoutSignature = Omit<
+ AmlDecisionRequest,
+ "officer_sig"
+>;
+
+export interface ExchangeVersionResponse {
+ // libtool-style representation of the Exchange 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-exchange";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v18, may become mandatory in the future.
+ implementation?: string;
+
+ // Currency supported by this exchange, given
+ // as a currency code ("USD" or "EUR").
+ currency: string;
+
+ // How wallets should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Names of supported KYC requirements.
+ supported_kyc_requirements: string[];
+}
+
+export interface WireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: PaytoString;
+
+ // 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: EddsaSignatureString;
+}
+
+export interface WalletKycRequest {
+ // Balance threshold (not necessarily exact balance)
+ // to be crossed by the wallet that (may) trigger
+ // additional KYC requirements.
+ balance: AmountString;
+
+ // EdDSA signature of the wallet affirming the
+ // request, must be of purpose
+ // TALER_SIGNATURE_WALLET_ACCOUNT_SETUP
+ reserve_sig: EddsaSignatureString;
+
+ // long-term wallet reserve-account
+ // public key used to create the signature.
+ reserve_pub: EddsaPublicKeyString;
+}
+
+export interface WalletKycCheckResponse {
+ // Next balance limit above which a KYC check
+ // may be required. Optional, not given if no
+ // threshold exists (assume infinity).
+ next_threshold?: AmountString;
+
+ // When does the current set of AML/KYC rules
+ // expire and the wallet needs to check again
+ // for updated thresholds.
+ expiration_time: Timestamp;
+}
+
+// Implemented in this style since exchange
+// protocol **v20**.
+export interface LegitimizationNeededResponse {
+ // Numeric error code unique to the condition.
+ // Should always be TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED.
+ 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;
+
+ // Hash of the payto:// account URI for which KYC
+ // is required.
+ // The account holder can uses the /kyc-check/$H_PAYTO
+ // endpoint to check the KYC status or initiate the KYC process.
+ h_payto: PaytoHash;
+
+ // Public key associated with the account. The client must sign
+ // the initial request for the KYC status using the corresponding
+ // private key. Will be either a reserve public key or a merchant
+ // (instance) public key.
+ //
+ // Absent if no public key is currently associated
+ // with the account and the client MUST thus first
+ // credit the exchange via an inbound wire transfer
+ // to associate a public key with the debited account.
+ account_pub?: EddsaPublicKeyString;
+
+ // Identifies a set of measures that were triggered and that are
+ // now preventing this operation from proceeding. Gives developers
+ // a starting point for understanding why the transaction was
+ // blocked and how to lift it.
+ // Can be zero (which means there is no requirement row),
+ // especially if bad_kyc_auth is set.
+ requirement_row: Integer;
+
+ // True if the operation was denied because the
+ // KYC auth key does not match the merchant public
+ // key. In this case, a KYC auth wire transfer
+ // with the merchant public key must be performed
+ // first.
+ // Since exchange protocol **v21**.
+ bad_kyc_auth?: boolean;
+}
+
+export interface AccountKycStatus {
+ // Current AML state for the target account. True if
+ // operations are not happening due to staff processing
+ // paperwork *or* due to legal requirements (so the
+ // client cannot do anything but wait).
+ //
+ // Note that not every AML staff action may be legally
+ // exposed to the client, so this is merely a hint that
+ // a client should be told that AML staff is currently
+ // reviewing the account. AML staff *may* review
+ // accounts without this flag being set!
+ aml_review: boolean;
+
+ // Access token needed to construct the /kyc-spa/
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (optional if the status
+ // type is 200 Ok, mandatory if the HTTP status
+ // is 202 Accepted).
+ access_token: AccessToken;
+
+ // Array with limitations that currently apply to this
+ // account and that may be increased or lifted if the
+ // KYC check is passed.
+ // Note that additional limits *may* exist and not be
+ // communicated to the client. If such limits are
+ // reached, this *may* be indicated by the account
+ // going into aml_review state. However, it is
+ // also possible that the exchange may legally have
+ // to deny operations without being allowed to provide
+ // any justification.
+ // The limits should be used by the client to
+ // possibly structure their operations (e.g. withdraw
+ // what is possible below the limit, ask the user to
+ // pass KYC checks or withdraw the rest after the time
+ // limit is passed, warn the user to not withdraw too
+ // much or even prevent the user from generating a
+ // request that would cause it to exceed hard limits).
+ limits?: AccountLimit[];
+}
+
+export type LimitOperationType = "WITHDRAW" | "DEPOSIT" | "MERGE" | "AGGREGATE" | "BALANCE" | "REFUND" | "CLOSE" | "TRANSACTION";
+
+export interface AccountLimit {
+ // Operation that is limited.
+ operation_type: LimitOperationType;
+
+ // Timeframe during which the limit applies.
+ timeframe: RelativeTime;
+
+ // Maximum amount allowed during the given timeframe.
+ // Zero if the operation is simply forbidden.
+ threshold: AmountString;
+
+ // True if this is a soft limit that could be raised
+ // by passing KYC checks. Clients *may* deliberately
+ // try to cross limits and trigger measures resulting
+ // in 451 responses to begin KYC processes.
+ // Clients that are aware of hard limits *should*
+ // inform users about the hard limit and prevent flows
+ // in the UI that would cause violations of hard limits.
+ // Made optional in **v21** with a default of 'false' if missing.
+ soft_limit?: boolean;
+}
+
+export interface KycProcessClientInformation {
+ // Array of requirements.
+ requirements: KycRequirementInformation[];
+
+ // True if the client is expected to eventually satisfy all requirements.
+ // Default (if missing) is false.
+ is_and_combinator?: boolean;
+
+ // List of available voluntary checks the client could pay for.
+ // Since **vATTEST**.
+ voluntary_checks?: { [name: string]: KycCheckPublicInformation };
+}
+
+declare const opaque_kycReq: unique symbol;
+export type KycRequirementInformationId = string & { [opaque_kycReq]: true };
+declare const opaque_formId: unique symbol;
+export type KycBuiltInFromId = string & { [opaque_formId]: true };
+
+export interface KycRequirementInformation {
+ // Which form should be used? Common values include "INFO"
+ // (to just show the descriptions but allow no action),
+ // "LINK" (to enable the user to obtain a link via
+ // /kyc-start/) or any built-in form name supported
+ // by the SPA.
+ form: "LINK" | "INFO" | KycBuiltInFromId;
+
+ // English description of the requirement.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // description texts.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // ID of the requirement, useful to construct the
+ // /kyc-upload/$ID or /kyc-start/$ID endpoint URLs.
+ // Present if and only if "form" is not "INFO". The
+ // $ID value may itself contain / or ? and
+ // basically encode any URL path (and optional arguments).
+ id?: KycRequirementInformationId;
+}
+
+// Since **vATTEST**.
+export interface KycCheckPublicInformation {
+ // English description of the check.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // description texts.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // FIXME: is the above in any way sufficient
+ // to begin the check? Do we not need at least
+ // something more??!?
+}
+
+export interface EventCounter {
+ // Number of events of the specified type in
+ // the given range.
+ counter: Integer;
+}
+
+export interface AmlDecisionsResponse {
+ // Array of AML decisions matching the query.
+ records: AmlDecision[];
+}
+
+export interface AmlDecision {
+ // Which payto-address is this record about.
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // Row ID of the record. Used to filter by offset.
+ rowid: Integer;
+
+ // Justification for the decision. NULL if none
+ // is available.
+ justification?: string;
+
+ // When was the decision made?
+ decision_time: Timestamp;
+
+ // Free-form properties about the account.
+ // Can be used to store properties such as PEP,
+ // risk category, type of business, hits on
+ // sanctions lists, etc.
+ properties?: AccountProperties;
+
+ // What are the new rules?
+ limits: LegitimizationRuleSet;
+
+ // True if the account is under investigation by AML staff
+ // after this decision.
+ to_investigate: boolean;
+
+ // True if this is the active decision for the
+ // account.
+ is_active: boolean;
+}
+
+// All fields in this object are optional. The actual
+// properties collected depend fully on the discretion
+// of the exchange operator;
+// however, some common fields are standardized
+// and thus described here.
+export interface AccountProperties {
+ // True if this is a politically exposed account.
+ // Rules for classifying accounts as politically
+ // exposed are country-dependent.
+ pep?: boolean;
+
+ // True if this is a sanctioned account.
+ // Rules for classifying accounts as sanctioned
+ // are country-dependent.
+ sanctioned?: boolean;
+
+ // True if this is a high-risk account.
+ // Rules for classifying accounts as at-risk
+ // are exchange operator-dependent.
+ high_risk?: boolean;
+
+ // Business domain of the account owner.
+ // The list of possible business domains is
+ // operator- or country-dependent.
+ business_domain?: string;
+
+ // Is the client's account currently frozen?
+ is_frozen?: boolean;
+
+ // Was the client's account reported to the authorities?
+ was_reported?: boolean;
+
+ /**
+ * Additional free-form properties.
+ */
+ [x: string]: any;
+}
+
+export interface LegitimizationRuleSet {
+ // When does this set of rules expire and
+ // we automatically transition to the successor
+ // measure?
+ expiration_time: Timestamp;
+
+ // Name of the measure to apply when the expiration time is
+ // reached. If not set, we refer to the default
+ // set of rules (and the default account state).
+ successor_measure?: string;
+
+ // Legitimization rules that are to be applied
+ // to this account.
+ rules: KycRule[];
+
+ // Custom measures that KYC rules and the
+ // successor_measure may refer to.
+ custom_measures: { [measure_name: string]: MeasureInformation };
+}
+
+export interface AmlDecisionRequest {
+ // Human-readable justification for the decision.
+ justification: string;
+
+ // Which payto-address is the decision about?
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // Payto address of the account the decision is about.
+ // Optional. Must be given if the account is not yet
+ // known to the exchange. If given, must match h_payto.
+ // New since protocol **v21**.
+ payto_uri?: string;
+
+ // What are the new rules?
+ // New since protocol **v20**.
+ new_rules: LegitimizationRuleSet;
+
+ // What are the new account properties?
+ // New since protocol **v20**.
+ properties: AccountProperties;
+
+ // Space-separated list of measures to trigger
+ // immediately on the account.
+ // Prefixed with a "+" to indicate that the
+ // measures should be ANDed.
+ // Should typically be used to give the user some
+ // information or request additional information.
+ // New since protocol **v21**.
+ new_measures?: string;
+
+ // True if the account should remain under investigation by AML staff.
+ // New since protocol **v20**.
+ keep_investigating: boolean;
+
+ // Signature by the AML officer over a TALER_AmlDecisionPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
+ officer_sig: EddsaSignatureString;
+
+ // When was the decision made?
+ decision_time: Timestamp;
+}
+
+export interface KycRule {
+ // Type of operation to which the rule applies.
+ operation_type: string;
+
+ // The measures will be taken if the given
+ // threshold is crossed over the given timeframe.
+ threshold: AmountString;
+
+ // Over which duration should the threshold be
+ // computed. All amounts of the respective
+ // operation_type will be added up for this
+ // duration and the sum compared to the threshold.
+ timeframe: RelativeTime;
+
+ // Array of names of measures to apply.
+ // Names listed can be original measures or
+ // custom measures from the AmlOutcome.
+ // A special measure "verboten" is used if the
+ // threshold may never be crossed.
+ measures: string[];
+
+ // If multiple rules apply to the same account
+ // at the same time, the number with the highest
+ // rule determines which set of measures will
+ // be activated and thus become visible for the
+ // user.
+ display_priority: Integer;
+
+ // True if the rule (specifically, operation_type,
+ // threshold, timeframe) and the general nature of
+ // the measures (verboten or approval required)
+ // should be exposed to the client.
+ // Defaults to "false" if not set.
+ exposed?: boolean;
+
+ // True if all the measures will eventually need to
+ // be satisfied, false if any of the measures should
+ // do. Primarily used by the SPA to indicate how
+ // the measures apply when showing them to the user;
+ // in the end, AML programs will decide after each
+ // measure what to do next.
+ // Default (if missing) is false.
+ is_and_combinator?: boolean;
+}
+
+export interface KycAttributes {
+ // Matching KYC attribute history of the account.
+ details: KycAttributeCollectionEvent[];
+}
+export interface KycAttributeCollectionEvent {
+ // Row ID of the record. Used to filter by offset.
+ rowid: Integer;
+
+ // Name of the provider
+ // which was used to collect the attributes. NULL if they were
+ // just uploaded via a form by the account owner.
+ provider_name?: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+}
+
+export enum AmlState {
+ normal = 0,
+ pending = 1,
+ frozen = 2,
+}
+type Float = number;
+
+export interface ExchangeKeysResponse {
+ // libtool-style representation of the Exchange protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // The exchange's base URL.
+ base_url: string;
+
+ // The exchange's currency or asset unit.
+ currency: string;
+
+ // How wallets should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Absolute cost offset for the STEFAN curve used
+ // to (over) approximate fees payable by amount.
+ stefan_abs: AmountString;
+
+ // Factor to multiply the logarithm of the amount
+ // with to (over) approximate fees payable by amount.
+ // Note that the total to be paid is first to be
+ // divided by the smallest denomination to obtain
+ // the value that the logarithm is to be taken of.
+ stefan_log: AmountString;
+
+ // Linear cost factor for the STEFAN curve used
+ // to (over) approximate fees payable by amount.
+ //
+ // Note that this is a scalar, as it is multiplied
+ // with the actual amount.
+ stefan_lin: Float;
+
+ // Type of the asset. "fiat", "crypto", "regional"
+ // or "stock". Wallets should adjust their UI/UX
+ // based on this value.
+ asset_type: string;
+
+ // Array of wire accounts operated by the exchange for
+ // incoming wire transfers.
+ accounts: WireAccount[];
+
+ // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank")
+ // to wire fees.
+ wire_fees: { method: AggregateTransferFee[] };
+
+ // List of exchanges that this exchange is partnering
+ // with to enable wallet-to-wallet transfers.
+ wads: ExchangePartnerListEntry[];
+
+ // EdDSA master public key of the exchange, used to sign entries
+ // in denoms and signkeys.
+ master_public_key: EddsaPublicKey;
+
+ // Relative duration until inactive reserves are closed;
+ // not signed (!), can change without notice.
+ reserve_closing_delay: RelativeTime;
+
+ // Threshold amounts beyond which wallet should
+ // trigger the KYC process of the issuing exchange.
+ // Optional option, if not given there is no limit.
+ // Currency must match currency.
+ wallet_balance_limit_without_kyc?: AmountString[];
+
+ // Array of limits that apply to all accounts.
+ // All of the given limits will be hard limits.
+ // Wallets and merchants are expected to obey them
+ // and not even allow the user to cross them.
+ // Since protocol **v21**.
+ hard_limits: AccountLimit[];
+
+ // Array of limits with a soft threshold of zero
+ // that apply to all accounts without KYC.
+ // Wallets and merchants are expected to trigger
+ // a KYC process before attempting any zero-limited
+ // operations.
+ // Since protocol **v21**.
+ zero_limits: ZeroLimitedOperation[];
+
+ // Denominations offered by this exchange
+ denominations: DenomGroup[];
+
+ // Compact EdDSA signature (binary-only) over the
+ // contatentation of all of the master_sigs (in reverse
+ // chronological order by group) in the arrays under
+ // "denominations". Signature of TALER_ExchangeKeySetPS
+ exchange_sig: EddsaSignature;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from signkeys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used for the exchange_sig.
+ exchange_pub: EddsaPublicKey;
+
+ // Denominations for which the exchange currently offers/requests recoup.
+ recoup: Recoup[];
+
+ // Array of globally applicable fees by time range.
+ global_fees: GlobalFees[];
+
+ // The date when the denomination keys were last updated.
+ list_issue_date: Timestamp;
+
+ // Auditors of the exchange.
+ auditors: AuditorKeys[];
+
+ // The exchange's signing keys.
+ signkeys: SignKey[];
+
+ // Optional field with a dictionary of (name, object) pairs defining the
+ // supported and enabled extensions, such as age_restriction.
+ extensions?: { name: ExtensionManifest };
+
+ // Signature by the exchange master key of the SHA-256 hash of the
+ // normalized JSON-object of field extensions, if it was set.
+ // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
+ extensions_sig?: EddsaSignature;
+}
+
+export interface ZeroLimitedOperation {
+ // Operation that is limited to an amount of
+ // zero until the client has passed some KYC check.
+ // Must be one of "WITHDRAW", "DEPOSIT",
+ // (p2p) "MERGE", (wallet) "BALANCE",
+ // (reserve) "CLOSE", "AGGREGATE",
+ // "TRANSACTION" or "REFUND".
+ operation_type: string;
+}
+
+interface ExtensionManifest {
+ // The criticality of the extension MUST be provided. It has the same
+ // semantics as "critical" has for extensions in X.509:
+ // - if "true", the client must "understand" the extension before
+ // proceeding,
+ // - if "false", clients can safely skip extensions they do not
+ // understand.
+ // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)
+ critical: boolean;
+
+ // The version information MUST be provided in Taler's protocol version
+ // ranges notation, see
+ // https://docs.taler.net/core/api-common.html#protocol-version-ranges
+ version: LibtoolVersionString;
+
+ // Optional configuration object, defined by the feature itself
+ config?: object;
+}
+
+interface SignKey {
+ // The actual exchange's EdDSA signing public key.
+ key: EddsaPublicKeyString;
+
+ // Initial validity date for the signing key.
+ stamp_start: Timestamp;
+
+ // Date when the exchange will stop using the signing key, allowed to overlap
+ // slightly with the next signing key's validity to allow for clock skew.
+ stamp_expire: Timestamp;
+
+ // Date when all signatures made by the signing key expire and should
+ // henceforth no longer be considered valid in legal disputes.
+ stamp_end: Timestamp;
+
+ // Signature over key and stamp_expire by the exchange master key.
+ // Signature of TALER_ExchangeSigningKeyValidityPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY.
+ master_sig: EddsaSignatureString;
+}
+
+interface AuditorKeys {
+ // The auditor's EdDSA signing public key.
+ auditor_pub: EddsaPublicKeyString;
+
+ // The auditor's URL.
+ auditor_url: string;
+
+ // The auditor's name (for humans).
+ auditor_name: string;
+
+ // An array of denomination keys the auditor affirms with its signature.
+ // Note that the message only includes the hash of the public key, while the
+ // signature is actually over the expanded information including expiration
+ // times and fees. The exact format is described below.
+ denomination_keys: AuditorDenominationKey[];
+}
+
+interface AuditorDenominationKey {
+ // Hash of the public RSA key used to sign coins of the respective
+ // denomination. Note that the auditor's signature covers more than just
+ // the hash, but this other information is already provided in denoms and
+ // thus not repeated here.
+ denom_pub_h: HashCodeString;
+
+ // Signature of TALER_ExchangeKeyValidityPS.
+ auditor_sig: EddsaSignatureString;
+}
+
+export interface GlobalFees {
+ // What date (inclusive) does these fees go into effect?
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fees stop going into effect?
+ end_date: Timestamp;
+
+ // Account history fee, charged when a user wants to
+ // obtain a reserve/account history.
+ history_fee: AmountString;
+
+ // Annual fee charged for having an open account at the
+ // exchange. Charged to the account. If the account
+ // balance is insufficient to cover this fee, the account
+ // is automatically deleted/closed. (Note that the exchange
+ // will keep the account history around for longer for
+ // regulatory reasons.)
+ account_fee: AmountString;
+
+ // Purse fee, charged only if a purse is abandoned
+ // and was not covered by the account limit.
+ purse_fee: AmountString;
+
+ // How long will the exchange preserve the account history?
+ // After an account was deleted/closed, the exchange will
+ // retain the account history for legal reasons until this time.
+ history_expiration: RelativeTime;
+
+ // Non-negative number of concurrent purses that any
+ // account holder is allowed to create without having
+ // to pay the purse_fee.
+ purse_account_limit: Integer;
+
+ // How long does an exchange keep a purse around after a purse
+ // has expired (or been successfully merged)? A 'GET' request
+ // for a purse will succeed until the purse expiration time
+ // plus this value.
+ purse_timeout: RelativeTime;
+
+ // Signature of TALER_GlobalFeesPS.
+ master_sig: EddsaSignatureString;
+}
+
+export interface AggregateTransferFee {
+ // Per transfer wire transfer fee.
+ wire_fee: AmountString;
+
+ // Per transfer closing fee.
+ closing_fee: AmountString;
+
+ // 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: EddsaSignatureString;
+}
+
+interface ExchangePartnerListEntry {
+ // Base URL of the partner exchange.
+ partner_base_url: string;
+
+ // Public master key of the partner exchange.
+ partner_master_pub: EddsaPublicKeyString;
+
+ // Per exchange-to-exchange transfer (wad) fee.
+ wad_fee: AmountString;
+
+ // Exchange-to-exchange wad (wire) transfer frequency.
+ wad_frequency: RelativeTime;
+
+ // When did this partnership begin (under these conditions)?
+ start_date: Timestamp;
+
+ // How long is this partnership expected to last?
+ end_date: Timestamp;
+
+ // Signature using the exchange's offline key over
+ // TALER_WadPartnerSignaturePS
+ // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS.
+ master_sig: EddsaSignatureString;
+}
+
+// Binary representation of the age groups.
+// The bits set in the mask mark the edges at the beginning of a next age
+// group. F.e. for the age groups
+// 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-*
+// the following bits are set:
+//
+// 31 24 16 8 0
+// | | | | |
+// oooooooo oo1oo1o1 o1o1o1o1 ooooooo1
+//
+// A value of 0 means that the exchange does not support the extension for
+// age-restriction.
+type AgeMask = Integer;
+
+type DenominationKey = RsaDenominationKey | CSDenominationKey;
+
+interface RsaDenominationKey {
+ cipher: "RSA";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // RSA public key
+ rsa_public_key: RsaPublicKey;
+}
+
+interface CSDenominationKey {
+ cipher: "CS";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // Public key of the denomination.
+ cs_public_key: Cs25519Point;
+}
+
+export const codecForExchangeConfig = (): Codec<ExchangeVersionResponse> =>
+ buildCodecForObject<ExchangeVersionResponse>()
+ .property("version", codecForString())
+ .property("name", codecForConstString("taler-exchange"))
+ .property("implementation", codecOptional(codecForURN()))
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("supported_kyc_requirements", codecForList(codecForString()))
+ .build("TalerExchangeApi.ExchangeVersionResponse");
+
+// FIXME: complete the codec to check for valid exchange response
+export const codecForExchangeKeys = (): Codec<ExchangeKeysResponse> =>
+ buildCodecForObject<ExchangeKeysResponse>()
+ .property("version", codecForString())
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .property("accounts", codecForAny())
+ .property("asset_type", codecForAny())
+ .property("auditors", codecForAny())
+ .property("currency_specification", codecForAny())
+ .property("zero_limits", codecForAny())
+ .property("hard_limits", codecForAny())
+ .property("denominations", codecForAny())
+ .property("exchange_pub", codecForAny())
+ .property("exchange_sig", codecForAny())
+ .property("extensions", codecForAny())
+ .property("extensions_sig", codecForAny())
+ .property("global_fees", codecForAny())
+ .property("list_issue_date", codecForAny())
+ .property("master_public_key", codecForAny())
+ .property("recoup", codecForAny())
+ .property("reserve_closing_delay", codecForAny())
+ .property("signkeys", codecForAny())
+ .property("stefan_abs", codecForAny())
+ .property("stefan_lin", codecForAny())
+ .property("stefan_log", codecForAny())
+ .property("wads", codecForAny())
+ .property("wallet_balance_limit_without_kyc", codecForAny())
+ .property("wire_fees", codecForAny())
+ .build("TalerExchangeApi.ExchangeKeysResponse");
+
+export const codecForEventCounter = (): Codec<EventCounter> =>
+ buildCodecForObject<EventCounter>()
+ .property("counter", codecForNumber())
+ .build("TalerExchangeApi.EventCounter");
+
+export const codecForAmlDecisionsResponse = (): Codec<AmlDecisionsResponse> =>
+ buildCodecForObject<AmlDecisionsResponse>()
+ .property("records", codecForList(codecForAmlDecision()))
+ .build("TalerExchangeApi.AmlDecisionsResponse");
+
+export const codecForAvailableMeasureSummary =
+ (): Codec<AvailableMeasureSummary> =>
+ buildCodecForObject<AvailableMeasureSummary>()
+ .property("checks", codecForMap(codecForKycCheckInformation()))
+ .property("programs", codecForMap(codecForAmlProgramRequirement()))
+ .property("roots", codecForMap(codecForMeasureInformation()))
+ .build("TalerExchangeApi.AvailableMeasureSummary");
+
+export const codecForAmlProgramRequirement = (): Codec<AmlProgramRequirement> =>
+ buildCodecForObject<AmlProgramRequirement>()
+ .property("description", codecForString())
+ .property("context", codecForList(codecForString()))
+ .property("inputs", codecForList(codecForString()))
+ .build("TalerExchangeApi.AmlProgramRequirement");
+
+export const codecForKycCheckInformation = (): Codec<KycCheckInformation> =>
+ buildCodecForObject<KycCheckInformation>()
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("fallback", codecForString())
+ .property("outputs", codecForList(codecForString()))
+ .property("requires", codecForList(codecForString()))
+ .build("TalerExchangeApi.KycCheckInformation");
+
+export const codecForMeasureInformation = (): Codec<MeasureInformation> =>
+ buildCodecForObject<MeasureInformation>()
+ .property("prog_name", codecForString())
+ .property("check_name", codecForString())
+ .property("context", codecForAny())
+ .property("operation_type", codecOptional(codecForString()))
+ .property("voluntary", codecOptional(codecForBoolean()))
+ .build("TalerExchangeApi.MeasureInformation");
+
+// export const codecForAmlDecisionDetails = (): Codec<AmlDecisionDetails> =>
+// buildCodecForObject<AmlDecisionDetails>()
+// .property("aml_history", codecForList(codecForAmlDecisionDetail()))
+// .property("kyc_attributes", codecForList(codecForKycDetail()))
+// .build("TalerExchangeApi.AmlDecisionDetails");
+
+// export const codecForAmlDecisionDetail = (): Codec<AmlDecisionDetail> =>
+// buildCodecForObject<AmlDecisionDetail>()
+// .property("justification", codecForString())
+// .property("new_state", codecForNumber())
+// .property("decision_time", codecForTimestamp)
+// .property("new_threshold", codecForAmountString())
+// .property("decider_pub", codecForString())
+// .build("TalerExchangeApi.AmlDecisionDetail");
+
+export const codecForKycDetail = (): Codec<KycDetail> =>
+ buildCodecForObject<KycDetail>()
+ .property("provider_section", codecForString())
+ .property("attributes", codecOptional(codecForAny()))
+ .property("collection_time", codecForTimestamp)
+ .property("expiration_time", codecForTimestamp)
+ .build("TalerExchangeApi.KycDetail");
+
+export const codecForAmlDecision = (): Codec<AmlDecision> =>
+ buildCodecForObject<AmlDecision>()
+ .property("h_payto", codecForString())
+ .property("rowid", codecForNumber())
+ .property("justification", codecOptional(codecForString()))
+ .property("decision_time", codecForTimestamp)
+ .property("properties", codecOptional(codecForAccountProperties()))
+ .property("limits", codecForLegitimizationRuleSet())
+ .property("to_investigate", codecForBoolean())
+ .property("is_active", codecForBoolean())
+ .build("TalerExchangeApi.AmlDecision");
+
+export const codecForAccountProperties = (): Codec<AccountProperties> =>
+ buildCodecForObject<AccountProperties>()
+ .property("pep", codecOptional(codecForBoolean()))
+ .property("sanctioned", codecOptional(codecForBoolean()))
+ .property("high_risk", codecOptional(codecForBoolean()))
+ .property("business_domain", codecOptional(codecForString()))
+ .property("is_frozen", codecOptional(codecForBoolean()))
+ .property("was_reported", codecOptional(codecForBoolean()))
+ .build("TalerExchangeApi.AccountProperties");
+
+export const codecForLegitimizationRuleSet = (): Codec<LegitimizationRuleSet> =>
+ buildCodecForObject<LegitimizationRuleSet>()
+ .property("expiration_time", codecForTimestamp)
+ .property("successor_measure", codecOptional(codecForString()))
+ .property("rules", codecForList(codecForKycRules()))
+ .property("custom_measures", codecForMap(codecForMeasureInformation()))
+ .build("TalerExchangeApi.LegitimizationRuleSet");
+
+export const codecForKycRules = (): Codec<KycRule> =>
+ buildCodecForObject<KycRule>()
+ .property("operation_type", codecForString())
+ .property("threshold", codecForAmountString())
+ .property("timeframe", codecForDuration)
+ .property("measures", codecForList(codecForString()))
+ .property("display_priority", codecForNumber())
+ .property("exposed", codecOptional(codecForBoolean()))
+ .property("is_and_combinator", codecOptional(codecForBoolean()))
+ .build("TalerExchangeApi.KycRule");
+
+export const codecForAmlKycAttributes = (): Codec<KycAttributes> =>
+ buildCodecForObject<KycAttributes>()
+ .property("details", codecForList(codecForKycAttributeCollectionEvent()))
+ .build("TalerExchangeApi.KycAttributes");
+
+export const codecForKycAttributeCollectionEvent =
+ (): Codec<KycAttributeCollectionEvent> =>
+ buildCodecForObject<KycAttributeCollectionEvent>()
+ .property("rowid", codecForNumber())
+ .property("provider_name", codecOptional(codecForString()))
+ .property("collection_time", codecForTimestamp)
+ .property("attributes", codecOptional(codecForAny()))
+ .build("TalerExchangeApi.KycAttributeCollectionEvent");
+
+export const codecForAmlWalletKycCheckResponse =
+ (): Codec<WalletKycCheckResponse> =>
+ buildCodecForObject<WalletKycCheckResponse>()
+ .property("next_threshold", codecOptional(codecForAmountString()))
+ .property("expiration_time", codecForTimestamp)
+ .build("TalerExchangeApi.WalletKycCheckResponse");
+
+export const codecForLegitimizationNeededResponse =
+ (): Codec<LegitimizationNeededResponse> =>
+ buildCodecForObject<LegitimizationNeededResponse>()
+ .property("code", codecForNumber())
+ .property("hint", codecOptional(codecForString()))
+ .property("h_payto", codecForString())
+ .property("account_pub", codecOptional(codecForString()))
+ .property("requirement_row", codecForNumber())
+ .property("bad_kyc_auth", codecOptional(codecForBoolean()))
+ .build("TalerExchangeApi.LegitimizationNeededResponse");
+
+export const codecForAccountKycStatus = (): Codec<AccountKycStatus> =>
+ buildCodecForObject<AccountKycStatus>()
+ .property("aml_review", codecForBoolean())
+ .property("access_token", codecForAccessToken())
+ .property("limits", codecOptional(codecForList(codecForAccountLimit())))
+ .build("TalerExchangeApi.AccountKycStatus");
+
+export const codecForOperationType = codecForEither(
+ codecForConstString("WITHDRAW"),
+ codecForConstString("DEPOSIT"),
+ codecForConstString("MERGE"),
+ codecForConstString("BALANCE"),
+ codecForConstString("CLOSE"),
+ codecForConstString("AGGREGATE"),
+ codecForConstString("TRANSACTION"),
+ codecForConstString("REFUND"),
+);
+
+export const codecForAccountLimit = (): Codec<AccountLimit> =>
+ buildCodecForObject<AccountLimit>()
+ .property(
+ "operation_type",
+ codecForOperationType,
+ )
+ .property("timeframe", codecForDuration)
+ .property("threshold", codecForAmountString())
+ .property("soft_limit", codecOptional(codecForBoolean()))
+ .build("TalerExchangeApi.AccountLimit");
+
+export const codecForZeroLimitedOperation = (): Codec<ZeroLimitedOperation> =>
+ buildCodecForObject<ZeroLimitedOperation>()
+ .property(
+ "operation_type",
+ codecForOperationType
+ )
+ .build("TalerExchangeApi.ZeroLimitedOperation");
+
+export const codecForKycCheckPublicInformation =
+ (): Codec<KycCheckPublicInformation> =>
+ buildCodecForObject<KycCheckPublicInformation>()
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .build("TalerExchangeApi.KycCheckPublicInformation");
+
+export const codecForKycRequirementInformationId =
+ (): Codec<KycRequirementInformationId> =>
+ codecForString() as Codec<KycRequirementInformationId>;
+export const codecForKycFormId = (): Codec<KycBuiltInFromId> =>
+ codecForString() as Codec<KycBuiltInFromId>;
+
+export const codecForKycRequirementInformation =
+ (): Codec<KycRequirementInformation> =>
+ buildCodecForObject<KycRequirementInformation>()
+ .property(
+ "form",
+ codecForEither(
+ codecForConstString("LINK"),
+ codecForConstString("INFO"),
+ codecForKycFormId(),
+ ),
+ )
+ .property("description", codecForString())
+ .property(
+ "description_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("id", codecOptional(codecForKycRequirementInformationId()))
+ .build("TalerExchangeApi.KycRequirementInformation");
+
+export const codecForKycProcessClientInformation =
+ (): Codec<KycProcessClientInformation> =>
+ buildCodecForObject<KycProcessClientInformation>()
+ .property(
+ "requirements",
+ codecOptionalDefault(
+ codecForList(codecForKycRequirementInformation()),
+ [],
+ ),
+ )
+ .property("is_and_combinator", codecOptional(codecForBoolean()))
+ .property(
+ "voluntary_checks",
+ codecOptional(codecForMap(codecForKycCheckPublicInformation())),
+ )
+ .build("TalerExchangeApi.KycProcessClientInformation");
+
+export interface KycProcessStartInformation {
+ // URL to open.
+ redirect_url: string;
+}
+
+export const codecForKycProcessStartInformation =
+ (): Codec<KycProcessStartInformation> =>
+ buildCodecForObject<KycProcessStartInformation>()
+ .property("redirect_url", codecForURLString())
+ .build("TalerExchangeApi.KycProcessStartInformation");
+
+export interface BatchWithdrawResponse {
+ // Array of blinded signatures, in the same order as was
+ // given in the request.
+ ev_sigs: WithdrawResponse[];
+}
+export interface WithdrawResponse {
+ // The blinded signature over the 'coin_ev', affirms the coin's
+ // validity after unblinding.
+ ev_sig: BlindedDenominationSignature;
+}
+export type BlindedDenominationSignature =
+ | RsaBlindedDenominationSignature
+ | CSBlindedDenominationSignature;
+
+export interface RsaBlindedDenominationSignature {
+ cipher: DenomKeyType.Rsa;
+
+ // (blinded) RSA signature
+ blinded_rsa_signature: BlindedRsaSignature;
+}
+
+export interface CSBlindedDenominationSignature {
+ cipher: DenomKeyType.ClauseSchnorr;
+
+ // Signer chosen bit value, 0 or 1, used
+ // in Clause Blind Schnorr to make the
+ // ROS problem harder.
+ b: Integer;
+
+ // Blinded scalar calculated from c_b.
+ s: Cs25519Scalar;
+}
+
+type BlindedRsaSignature = string;
+type Cs25519Scalar = string;
+type HashCode = string;
+type EddsaSignature = string;
+type EddsaPublicKey = string;
+type Amount = AmountString;
+type Base32 = string;
+
+export interface WithdrawError {
+ // Text describing the error.
+ hint: string;
+
+ // Detailed error code.
+ code: Integer;
+
+ // Amount left in the reserve.
+ balance: AmountString;
+
+ // History of the reserve's activity, in the same format
+ // as returned by /reserve/$RID/history.
+ history: TransactionHistoryItem[];
+}
+export type TransactionHistoryItem =
+ | AccountSetupTransaction
+ | ReserveWithdrawTransaction
+ | ReserveAgeWithdrawTransaction
+ | ReserveCreditTransaction
+ | ReserveClosingTransaction
+ | ReserveOpenRequestTransaction
+ | ReserveCloseRequestTransaction
+ | PurseMergeTransaction;
+
+enum TransactionHistoryType {
+ setup = "SETUP",
+ withdraw = "WITHDRAW",
+ ageWithdraw = "AGEWITHDRAW",
+ credit = "CREDIT",
+ closing = "CLOSING",
+ open = "OPEN",
+ close = "CLOSE",
+ merge = "MERGE",
+}
+interface AccountSetupTransaction {
+ type: TransactionHistoryType.setup;
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // KYC fee agreed to by the reserve owner.
+ kyc_fee: AmountString;
+
+ // Time when the KYC was triggered.
+ kyc_timestamp: Timestamp;
+
+ // Hash of the wire details of the account.
+ // Note that this hash is unsalted and potentially
+ // private (as it could be inverted), hence access
+ // to this endpoint must be authorized using the
+ // private key of the reserve.
+ h_wire: HashCode;
+
+ // Signature created with the reserve's private key.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST over
+ // a TALER_AccountSetupRequestSignaturePS.
+ reserve_sig: EddsaSignature;
+}
+interface ReserveWithdrawTransaction {
+ type: "WITHDRAW";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // Amount withdrawn.
+ amount: Amount;
+
+ // Hash of the denomination public key of the coin.
+ h_denom_pub: HashCode;
+
+ // Hash of the blinded coin to be signed.
+ h_coin_envelope: HashCode;
+
+ // Signature over a TALER_WithdrawRequestPS
+ // with purpose TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW
+ // created with the reserve's private key.
+ reserve_sig: EddsaSignature;
+
+ // Fee that is charged for withdraw.
+ withdraw_fee: Amount;
+}
+interface ReserveAgeWithdrawTransaction {
+ type: "AGEWITHDRAW";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // Total Amount withdrawn.
+ amount: Amount;
+
+ // Commitment of all n*kappa blinded coins.
+ h_commitment: HashCode;
+
+ // Signature over a TALER_AgeWithdrawRequestPS
+ // with purpose TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW
+ // created with the reserve's private key.
+ reserve_sig: EddsaSignature;
+
+ // Fee that is charged for withdraw.
+ withdraw_fee: Amount;
+}
+interface ReserveCreditTransaction {
+ type: "CREDIT";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // Amount deposited.
+ amount: Amount;
+
+ // Sender account payto:// URL.
+ sender_account_url: string;
+
+ // Opaque identifier internal to the exchange that
+ // uniquely identifies the wire transfer that credited the reserve.
+ wire_reference: Integer;
+
+ // Timestamp of the incoming wire transfer.
+ timestamp: Timestamp;
+}
+interface ReserveClosingTransaction {
+ type: "CLOSING";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // Closing balance.
+ amount: Amount;
+
+ // Closing fee charged by the exchange.
+ closing_fee: Amount;
+
+ // Wire transfer subject.
+ wtid: Base32;
+
+ // payto:// URI of the wire account into which the funds were returned to.
+ receiver_account_details: string;
+
+ // This is a signature over a
+ // struct TALER_ReserveCloseConfirmationPS with purpose
+ // TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED.
+ exchange_sig: EddsaSignature;
+
+ // Public key used to create 'exchange_sig'.
+ exchange_pub: EddsaPublicKey;
+
+ // Time when the reserve was closed.
+ timestamp: Timestamp;
+}
+interface ReserveOpenRequestTransaction {
+ type: "OPEN";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // Open fee paid from the reserve.
+ open_fee: Amount;
+
+ // This is a signature over
+ // a struct TALER_ReserveOpenPS with purpose
+ // TALER_SIGNATURE_WALLET_RESERVE_OPEN.
+ reserve_sig: EddsaSignature;
+
+ // Timestamp of the open request.
+ request_timestamp: Timestamp;
+
+ // Requested expiration.
+ requested_expiration: Timestamp;
+
+ // Requested number of free open purses.
+ requested_min_purses: Integer;
+}
+interface ReserveCloseRequestTransaction {
+ type: "CLOSE";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // This is a signature over
+ // a struct TALER_ReserveClosePS with purpose
+ // TALER_SIGNATURE_WALLET_RESERVE_CLOSE.
+ reserve_sig: EddsaSignature;
+
+ // Target account payto://, optional.
+ h_payto?: PaytoHash;
+
+ // Timestamp of the close request.
+ request_timestamp: Timestamp;
+}
+interface PurseMergeTransaction {
+ type: "MERGE";
+
+ // Offset of this entry in the reserve history.
+ // Useful to request incremental histories via
+ // the "start" query parameter.
+ history_offset: Integer;
+
+ // SHA-512 hash of the contact of the purse.
+ h_contract_terms: HashCode;
+
+ // EdDSA public key used to approve merges of this purse.
+ merge_pub: EddsaPublicKey;
+
+ // Minimum age required for all coins deposited into the purse.
+ min_age: Integer;
+
+ // Number that identifies who created the purse
+ // and how it was paid for.
+ flags: Integer;
+
+ // Purse public key.
+ purse_pub: EddsaPublicKey;
+
+ // EdDSA signature of the account/reserve affirming the merge
+ // over a TALER_AccountMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
+ reserve_sig: EddsaSignature;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: Timestamp;
+
+ // Indicative time by which the purse should expire
+ // if it has not been merged into an account. At this
+ // point, all of the deposits made should be
+ // auto-refunded.
+ purse_expiration: Timestamp;
+
+ // Purse fee the reserve owner paid for the purse creation.
+ purse_fee: Amount;
+
+ // Total amount merged into the reserve.
+ // (excludes fees).
+ amount: Amount;
+
+ // True if the purse was actually merged.
+ // If false, only the purse_fee has an impact
+ // on the reserve balance!
+ merged: boolean;
+}
+
+interface DenominationExpiredMessage {
+ // Taler error code. Note that beyond
+ // expiration this message format is also
+ // used if the key is not yet valid, or
+ // has been revoked.
+ code: number;
+
+ // Signature by the exchange over a
+ // TALER_DenominationExpiredAffirmationPS.
+ // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
+ exchange_sig: EddsaSignature;
+
+ // Public key of the exchange used to create
+ // the 'exchange_sig.
+ exchange_pub: EddsaPublicKey;
+
+ // Hash of the denomination public key that is unknown.
+ h_denom_pub: HashCode;
+
+ // When was the signature created.
+ timestamp: Timestamp;
+
+ // What kind of operation was requested that now
+ // failed?
+ oper: string;
+}
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
new file mode 100644
index 000000000..a737b7994
--- /dev/null
+++ b/packages/taler-util/src/types-taler-merchant.ts
@@ -0,0 +1,3485 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { codecForAmountString } from "./amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForBoolean,
+ codecForList,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import {
+ AccessToken,
+ AccountLimit,
+ CoinEnvelope,
+ ExchangeWireAccount,
+ PaytoString,
+ buildCodecForUnion,
+ codecForAccountLimit,
+ codecForConstNumber,
+ codecForConstString,
+ codecForEither,
+ codecForExchangeWireAccount,
+ codecForMap,
+ codecForPaytoString,
+ codecForTalerUriString,
+} from "./index.js";
+import {
+ AbsoluteTime,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
+} from "./time.js";
+import {
+ AmountString,
+ Base32String,
+ BlindedRsaSignature,
+ ClaimToken,
+ CoinPublicKey,
+ CurrencySpecification,
+ EddsaPublicKey,
+ EddsaPublicKeyString,
+ EddsaSignatureString,
+ HashCode,
+ HashCodeString,
+ ImageDataUrl,
+ Integer,
+ InternationalizedString,
+ RelativeTime,
+ RsaSignature,
+ Timestamp,
+ WireTransferIdentifierRawP,
+ codecForAccessToken,
+ codecForCurrencySpecificiation,
+ codecForInternationalizedString,
+ codecForURLString,
+} from "./types-taler-common.js";
+
+/**
+ * Proposal returned from the contract URL.
+ */
+export interface Proposal {
+ /**
+ * Contract terms for the propoal.
+ * Raw, un-decoded JSON object.
+ */
+ contract_terms: any;
+
+ /**
+ * Signature over contract, made by the merchant. The public key used for signing
+ * must be contract_terms.merchant_pub.
+ */
+ sig: string;
+}
+
+export interface MerchantPayResponse {
+ sig: string;
+ pos_confirmation?: string;
+}
+
+interface MerchantOrderStatusPaid {
+ // Was the payment refunded (even partially, via refund or abort)?
+ refunded: boolean;
+
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
+}
+
+interface MerchantOrderRefundResponse {
+ /**
+ * Amount that was refunded in total.
+ */
+ refund_amount: AmountString;
+
+ /**
+ * Successful refunds for this payment, empty array for none.
+ */
+ refunds: MerchantCoinRefundStatus[];
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: EddsaPublicKeyString;
+}
+
+/**
+ * Response from the internal merchant API.
+ */
+export class CheckPaymentResponse {
+ order_status: string;
+ refunded: boolean | undefined;
+ refunded_amount: string | undefined;
+ contract_terms: any | undefined;
+ taler_pay_uri: string | undefined;
+ contract_url: string | undefined;
+}
+
+export const codecForMerchantRefundPermission =
+ (): Codec<MerchantAbortPayRefundDetails> =>
+ buildCodecForObject<MerchantAbortPayRefundDetails>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_fee", codecForAmountString())
+ .property("coin_pub", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("exchange_sig", codecOptional(codecForString()))
+ .property("exchange_pub", codecOptional(codecForString()))
+ .build("MerchantRefundPermission");
+
+export const codecForProposal = (): Codec<Proposal> =>
+ buildCodecForObject<Proposal>()
+ .property("contract_terms", codecForAny())
+ .property("sig", codecForString())
+ .build("Proposal");
+
+export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
+ buildCodecForObject<CheckPaymentResponse>()
+ .property("order_status", codecForString())
+ .property("refunded", codecOptional(codecForBoolean()))
+ .property("refunded_amount", codecOptional(codecForString()))
+ .property("contract_terms", codecOptional(codecForAny()))
+ .property("taler_pay_uri", codecOptional(codecForString()))
+ .property("contract_url", codecOptional(codecForString()))
+ .build("CheckPaymentResponse");
+
+export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+
+export interface MerchantCoinRefundSuccessStatus {
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // the EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund
+ exchange_sig: EddsaSignatureString;
+
+ // public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: TalerProtocolTimestamp;
+}
+
+export interface MerchantCoinRefundFailureStatus {
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: any;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: TalerProtocolTimestamp;
+}
+
+export interface MerchantOrderStatusUnpaid {
+ /**
+ * 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;
+}
+
+export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
+ buildCodecForObject<MerchantPayResponse>()
+ .property("sig", codecForString())
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .build("MerchantPayResponse");
+
+export const codecForMerchantOrderStatusPaid =
+ (): Codec<MerchantOrderStatusPaid> =>
+ buildCodecForObject<MerchantOrderStatusPaid>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_taken", codecForAmountString())
+ .property("refund_pending", codecForBoolean())
+ .property("refunded", codecForBoolean())
+ .build("MerchantOrderStatusPaid");
+
+export const codecForMerchantOrderStatusUnpaid =
+ (): Codec<MerchantOrderStatusUnpaid> =>
+ buildCodecForObject<MerchantOrderStatusUnpaid>()
+ .property("taler_pay_uri", codecForString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .build("MerchantOrderStatusUnpaid");
+
+export interface AbortRequest {
+ // hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer in case $ORDER_ID is guessable).
+ h_contract: string;
+
+ // List of coins the wallet would like to see refunds for.
+ // (Should be limited to the coins for which the original
+ // payment succeeded, as far as the wallet knows.)
+ coins: AbortingCoin[];
+}
+
+export interface AbortingCoin {
+ // Public key of a coin for which the wallet is requesting an abort-related refund.
+ coin_pub: EddsaPublicKeyString;
+
+ // The amount to be refunded (matches the original contribution)
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+}
+
+export interface AbortResponse {
+ // List of refund responses about the coins that the wallet
+ // requested an abort for. In the same order as the 'coins'
+ // from the original request.
+ // The rtransaction_id is implied to be 0.
+ refunds: MerchantAbortPayRefundStatus[];
+}
+
+export type MerchantAbortPayRefundStatus =
+ | MerchantAbortPayRefundSuccessStatus
+ | MerchantAbortPayRefundFailureStatus;
+
+// Details about why a refund failed.
+export interface MerchantAbortPayRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: unknown;
+}
+
+// Additional details needed to verify the refund confirmation signature
+// (h_contract_terms and merchant_pub) are already known
+// to the wallet and thus not included.
+export interface MerchantAbortPayRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // the EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund
+ exchange_sig: string;
+
+ // public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: string;
+}
+
+export interface AuditorHandle {
+ /**
+ * Official name of the auditor.
+ */
+ name: string;
+
+ /**
+ * Master public signing key of the auditor.
+ */
+ auditor_pub: EddsaPublicKeyString;
+
+ /**
+ * Base URL of the auditor.
+ */
+ url: string;
+}
+
+// Delivery location, loosely modeled as a subset of
+// ISO20022's PostalAddress25.
+export 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[];
+}
+
+export interface MerchantInfo {
+ // The merchant's legal name of business.
+ name: string;
+
+ // Label for a location with the business address of the merchant.
+ email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // 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;
+}
+
+export interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: AmountString;
+}
+
+export 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?: InternationalizedString;
+
+ // 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?: AmountString;
+
+ // 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;
+}
+
+/**
+ * Contract terms from a merchant.
+ * FIXME: Add type field!
+ */
+export interface MerchantContractTerms {
+ // The hash of the merchant instance's wire details.
+ h_wire: 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?: TalerProtocolDuration;
+
+ // 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;
+
+ // Human-readable description of the whole purchase.
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries.
+ summary_i18n?: InternationalizedString;
+
+ // 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: string;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // After this deadline, the merchant won't accept payments for the contract.
+ pay_deadline: TalerProtocolTimestamp;
+
+ // More info about the merchant, see below.
+ merchant: MerchantInfo;
+
+ // 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: string;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
+ exchanges: Exchange[];
+
+ // List of products that are part of the purchase (see Product).
+ products?: Product[];
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_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;
+
+ // Time when this contract was generated.
+ timestamp: TalerProtocolTimestamp;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional, but either fulfillment_url
+ // or fulfillment_message must be specified in every
+ // contract terms.
+ //
+ // If a non-unique fulfillment URL is used, a customer can only
+ // buy the order once and will be redirected to a previous purchase
+ // when trying to buy an order with the same fulfillment URL a second
+ // time. This is useful for digital goods that a customer only needs
+ // to buy once but should be able to repeatedly download.
+ //
+ // For orders where the customer is expected to be able to make
+ // repeated purchases (for equivalent goods), the fulfillment URL
+ // should be made unique for every order. The easiest way to do
+ // this is to include a unique order ID in the fulfillment URL.
+ //
+ // When POSTing to the merchant, the placeholder text "${ORDER_ID}"
+ // is be replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL). Note that this placeholder can only be used once.
+ // Front-ends may use other means to generate a unique fulfillment URL.
+ fulfillment_url?: string;
+
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
+ public_reorder_url?: string;
+
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
+ fulfillment_message_i18n?: InternationalizedString;
+
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: string;
+
+ // 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.
+ // Must really be an Object (not a string, integer, float or array).
+ extra?: any;
+
+ // Minimum age the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
+}
+
+/**
+ * Refund permission in the format that the merchant gives it to us.
+ */
+export interface MerchantAbortPayRefundDetails {
+ /**
+ * Amount to be refunded.
+ */
+ refund_amount: string;
+
+ /**
+ * Fee for the refund.
+ */
+ refund_fee: string;
+
+ /**
+ * Public key of the coin being refunded.
+ */
+ coin_pub: string;
+
+ /**
+ * Refund transaction ID between merchant and exchange.
+ */
+ rtransaction_id: number;
+
+ /**
+ * Exchange's key used for the signature.
+ */
+ exchange_pub?: string;
+
+ /**
+ * Exchange's signature to confirm the refund.
+ */
+ exchange_sig?: string;
+
+ /**
+ * Error replay from the exchange (if any).
+ */
+ exchange_reply?: any;
+
+ /**
+ * Error code from the exchange (if any).
+ */
+ exchange_code?: number;
+
+ /**
+ * HTTP status code of the exchange's response
+ * to the merchant's refund request.
+ */
+ exchange_http_status: number;
+}
+
+/**
+ * Reserve signature, defined as separate class to facilitate
+ * schema validation.
+ */
+export interface MerchantBlindSigWrapperV1 {
+ /**
+ * Reserve signature.
+ */
+ blind_sig: string;
+}
+
+export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
+ buildCodecForObject<AuditorHandle>()
+ .property("name", codecForString())
+ .property("auditor_pub", codecForString())
+ .property("url", codecForString())
+ .build("AuditorHandle");
+
+export const codecForLocation = (): Codec<Location> =>
+ buildCodecForObject<Location>()
+ .property("country", codecOptional(codecForString()))
+ .property("country_subdivision", codecOptional(codecForString()))
+ .property("building_name", codecOptional(codecForString()))
+ .property("building_number", codecOptional(codecForString()))
+ .property("district", codecOptional(codecForString()))
+ .property("street", codecOptional(codecForString()))
+ .property("post_code", codecOptional(codecForString()))
+ .property("town", codecOptional(codecForString()))
+ .property("town_location", codecOptional(codecForString()))
+ .property("address_lines", codecOptional(codecForList(codecForString())))
+ .build("Location");
+
+export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
+ buildCodecForObject<MerchantInfo>()
+ .property("name", codecForString())
+ .property("address", codecOptional(codecForLocation()))
+ .property("jurisdiction", codecOptional(codecForLocation()))
+ .build("MerchantInfo");
+
+export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
+ buildCodecForObject<MerchantContractTerms>()
+ .property("order_id", codecForString())
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("fulfillment_message", codecOptional(codecForString()))
+ .property(
+ "fulfillment_message_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("merchant_base_url", codecForString())
+ .property("h_wire", codecForString())
+ .property("auto_refund", codecOptional(codecForDuration))
+ .property("wire_method", codecForString())
+ .property("summary", codecForString())
+ .property("summary_i18n", codecOptional(codecForInternationalizedString()))
+ .property("nonce", codecForString())
+ .property("amount", codecForAmountString())
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("delivery_location", codecOptional(codecForLocation()))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .property("max_fee", codecForAmountString())
+ .property("merchant", codecForMerchantInfo())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchange()))
+ .property("products", codecOptional(codecForList(codecForProduct())))
+ .property("extra", codecForAny())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("MerchantContractTerms");
+
+export interface TalerMerchantConfigResponse {
+ // 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";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since **v8**, may become mandatory in the future.
+ implementation?: string;
+
+ // Default (!) currency supported by this backend.
+ // This is the currency that the backend should
+ // suggest by default to the user when entering
+ // amounts. See currencies for a list of
+ // supported currencies and how to render them.
+ currency: string;
+
+ // How services should render currencies supported
+ // by this backend. Maps
+ // currency codes (e.g. "EUR" or "KUDOS") to
+ // the respective currency specification.
+ // All currencies in this map are supported by
+ // the backend. Note that the actual currency
+ // specifications are a *hint* for applications
+ // that would like *advice* on how to render amounts.
+ // Applications *may* ignore the currency specification
+ // if they know how to render currencies that they are
+ // used with.
+ currencies: { [currency: string]: CurrencySpecification };
+
+ // Array of exchanges trusted by the merchant.
+ // Since protocol **v6**.
+ exchanges: ExchangeConfigInfo[];
+}
+
+export interface ExchangeConfigInfo {
+ // Base URL of the exchange REST API.
+ base_url: string;
+
+ // Currency for which the merchant is configured
+ // to trust the exchange.
+ // May not be the one the exchange actually uses,
+ // but is the only one we would trust this exchange for.
+ currency: string;
+
+ // Offline master public key of the exchange. The
+ // /keys data must be signed with this public
+ // key for us to trust it.
+ master_pub: EddsaPublicKey;
+}
+
+export interface ClaimRequest {
+ // Nonce to identify the wallet that claimed the order.
+ nonce: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // *Optional* as the merchant may not have required it
+ // (create_token set to false in PostOrderRequest).
+ token?: ClaimToken;
+}
+
+export interface ClaimResponse {
+ // Contract terms of the claimed order
+ contract_terms: ContractTerms;
+
+ // Signature by the merchant over the contract terms.
+ sig: EddsaSignatureString;
+}
+
+export interface PaymentResponse {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignatureString;
+
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment.
+ pos_confirmation?: string;
+}
+export interface PaymentDeniedLegallyResponse {
+ // Base URL of the exchanges that denied the payment.
+ // The wallet should refresh the coins from these
+ // exchanges, but may try to pay with coins from
+ // other exchanges.
+ exchange_base_urls: string[];
+}
+
+export interface PaymentStatusRequestParams {
+ // Hash of the order’s contract terms (this is used to
+ // authenticate the wallet/customer in case
+ // $ORDER_ID is guessable).
+ // Required once an order was claimed.
+ contractTermHash?: string;
+ // Authorizes the request via the claim token that
+ // was returned in the PostOrderResponse. Used with
+ // unclaimed orders only. Whether token authorization is
+ // required is determined by the merchant when the
+ // frontend creates the order.
+ claimToken?: string;
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // If specified, the merchant backend will wait up to
+ // timeout_ms milliseconds for completion of the payment
+ // before sending the HTTP response. A client must never
+ // rely on this behavior, as the merchant backend may return
+ // a response immediately.
+ timeout?: number;
+ // If set to “yes”, poll for the order’s pending refunds
+ // to be picked up. timeout_ms specifies how long we
+ // will wait for the refund.
+ awaitRefundObtained?: boolean;
+ // Indicates that we are polling for a refund above the
+ // given AMOUNT. timeout_ms will specify how long we
+ // will wait for the refund.
+ refund?: AmountString;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+}
+export interface GetKycStatusRequestParams {
+ // If specified, the KYC check should return
+ // the KYC status only for this wire account.
+ // Otherwise, for all wire accounts.
+ wireHash?: string;
+ // If specified, the KYC check should return
+ // the KYC status only for the given exchange.
+ // Otherwise, for all exchanges we interacted with.
+ exchangeURL?: string;
+ // If specified, the merchant will wait up to
+ // timeout_ms milliseconds for the exchanges to
+ // confirm completion of the KYC process(es).
+ timeout?: number;
+}
+export interface GetOtpDeviceRequestParams {
+ // Timestamp in seconds to use when calculating
+ // the current OTP code of the device. Since protocol v10.
+ faketime?: number;
+ // Price to use when calculating the current OTP
+ // code of the device. Since protocol v10.
+ price?: AmountString;
+}
+export interface GetOrderRequestParams {
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // Timeout in milliseconds to wait for a payment if
+ // the answer would otherwise be negative (long polling).
+ timeout?: number;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+}
+export interface ListWireTransferRequestParams {
+ // Filter for transfers to the given bank account
+ // (subject and amount MUST NOT be given in the payto URI).
+ paytoURI?: string;
+ // Filter for transfers executed before the given timestamp.
+ before?: number;
+ // Filter for transfers executed after the given timestamp.
+ after?: number;
+ // At most return the given number of results. Negative for
+ // descending in execution time, positive for ascending in
+ // execution time. Default is -20.
+ limit?: number;
+ // Starting transfer_serial_id for an iteration.
+ offset?: string;
+ // Filter transfers by verification status.
+ verified?: boolean;
+ order?: "asc" | "dec";
+}
+export interface ListOrdersRequestParams {
+ // If set to yes, only return paid orders, if no only
+ // unpaid orders. Do not give (or use “all”) to see all
+ // orders regardless of payment status.
+ paid?: boolean;
+ // If set to yes, only return refunded orders, if no only
+ // unrefunded orders. Do not give (or use “all”) to see
+ // all orders regardless of refund status.
+ refunded?: boolean;
+ // If set to yes, only return wired orders, if no only
+ // orders with missing wire transfers. Do not give (or
+ // use “all”) to see all orders regardless of wire transfer
+ // status.
+ wired?: boolean;
+ // At most return the given number of results. Negative
+ // for descending by row ID, positive for ascending by
+ // row ID. Default is 20. Since protocol v12.
+ limit?: number;
+ // Non-negative date in seconds after the UNIX Epoc, see delta
+ // for its interpretation. If not specified, we default to the
+ // oldest or most recent entry, depending on delta.
+ date?: AbsoluteTime;
+ // Starting product_serial_id for an iteration.
+ // Since protocol v12.
+ offset?: string;
+ // Timeout in milliseconds to wait for additional orders if the
+ // answer would otherwise be negative (long polling). Only useful
+ // if delta is positive. Note that the merchant MAY still return
+ // a response that contains fewer than delta orders.
+ timeout?: number;
+ // Since protocol v6. Filters by session ID.
+ sessionId?: string;
+ // Since protocol v6. Filters by fulfillment URL.
+ fulfillmentUrl?: string;
+
+ order?: "asc" | "dec";
+}
+
+export interface PayRequest {
+ // The coins used to make the payment.
+ coins: CoinPaySig[];
+
+ // Custom inputs from the wallet for the contract.
+ wallet_data?: Object;
+
+ // The session for which the payment is made (or replayed).
+ // Only set for session-based payments.
+ session_id?: string;
+}
+
+export interface CoinPaySig {
+ // Signature by the coin.
+ coin_sig: EddsaSignatureString;
+
+ // Public key of the coin being spent.
+ coin_pub: EddsaPublicKey;
+
+ // Signature made by the denomination public key.
+ ub_sig: RsaSignature;
+
+ // The hash of the denomination public key associated with this coin.
+ h_denom: HashCodeString;
+
+ // The amount that is subtracted from this coin with this payment.
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+}
+
+export interface StatusPaid {
+ type: "paid";
+
+ // Was the payment refunded (even partially, via refund or abort)?
+ refunded: boolean;
+
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
+}
+export interface StatusGotoResponse {
+ type: "goto";
+ // The client should go to the reorder URL, there a fresh
+ // order might be created as this one is taken by another
+ // customer or wallet (or repurchase detection logic may
+ // apply).
+ public_reorder_url: string;
+}
+export interface StatusUnpaidResponse {
+ type: "unpaid";
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ fulfillment_url?: 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;
+}
+
+export interface PaidRefundStatusResponse {
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment (present only if reusable OTP algorithm is used).
+ pos_confirmation?: string;
+
+ // True if the order has been subjected to
+ // refunds. False if it was simply paid.
+ refunded: boolean;
+}
+
+export interface PaidRequest {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignatureString;
+
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer and to enable signature verification without
+ // database access).
+ h_contract: HashCodeString;
+
+ // Hash over custom inputs from the wallet for the contract.
+ wallet_data_hash?: HashCodeString;
+
+ // Session id for which the payment is proven.
+ session_id: string;
+}
+
+export interface AbortRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer in case $ORDER_ID is guessable).
+ h_contract: HashCodeString;
+
+ // List of coins the wallet would like to see refunds for.
+ // (Should be limited to the coins for which the original
+ // payment succeeded, as far as the wallet knows.)
+ coins: AbortingCoin[];
+}
+
+export interface AbortResponse {
+ // List of refund responses about the coins that the wallet
+ // requested an abort for. In the same order as the coins
+ // from the original request.
+ // The rtransaction_id is implied to be 0.
+ refunds: MerchantAbortPayRefundStatus[];
+}
+
+export interface WalletRefundRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer).
+ h_contract: HashCodeString;
+}
+
+export interface WalletRefundResponse {
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Successful refunds for this payment, empty array for none.
+ refunds: MerchantCoinRefundStatus[];
+
+ // Public key of the merchant.
+ merchant_pub: EddsaPublicKey;
+}
+
+// Additional details needed to verify the refund confirmation signature
+// (h_contract_terms and merchant_pub) are already known
+// to the wallet and thus not included.
+export interface MerchantCoinRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKey;
+
+ // Refund transaction ID.
+ rtransaction_id: Integer;
+
+ // Public key of a coin that was refunded.
+ coin_pub: EddsaPublicKey;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ // Timestamp when the merchant approved the refund.
+ // Useful for grouping refunds.
+ execution_time: Timestamp;
+}
+
+interface RewardInformation {
+ // Exchange from which the reward will be withdrawn. Needed by the
+ // wallet to determine denominations, fees, etc.
+ exchange_url: string;
+
+ // URL where to go after obtaining the reward.
+ next_url: string;
+
+ // (Remaining) amount of the reward (including fees).
+ reward_amount: AmountString;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ // Note that rewards that have expired MAY also result in a 404 response.
+ expiration: Timestamp;
+}
+
+interface PlanchetDetail {
+ // Hash of the denomination's public key (hashed to reduce
+ // bandwidth consumption).
+ denom_pub_hash: HashCodeString;
+
+ // Coin's blinded public key.
+ coin_ev: CoinEnvelope;
+}
+
+interface BlindSignature {
+ // The (blind) RSA signature. Still needs to be unblinded.
+ blind_sig: BlindedRsaSignature;
+}
+
+export interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ // Must match the regex ^[A-Za-z0-9][A-Za-z0-9_.@-]+$.
+ 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?: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Authentication settings for this instance
+ 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;
+}
+
+export 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 "token", 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?: AccessToken;
+}
+
+export 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?: string;
+
+ // 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;
+}
+
+export interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance).
+ instances: Instance[];
+}
+
+export interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // 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;
+}
+
+export interface QueryInstancesResponse {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // 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";
+ };
+}
+export interface MerchantAccountKycRedirectsResponse {
+ // Array of KYC status information for
+ // the exchanges and bank accounts selected
+ // by the query.
+ kyc_data: MerchantAccountKycRedirect[];
+}
+
+export interface MerchantAccountKycRedirect {
+ // Our bank wire account this is about.
+ payto_uri: string;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // Since protocol **v17**.
+ exchange_http_status: number;
+
+ // Set to true if we did not get a /keys response from
+ // the exchange and thus cannot do certain checks, such as
+ // determining default account limits or account eligibility.
+ no_keys: boolean;
+
+ // Set to true if the given account cannot to KYC at the
+ // given exchange because no wire method exists that could
+ // be used to do the KYC auth wire transfer.
+ auth_conflict: boolean;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ // Optional (as there may not always have
+ // been an error code). Since protocol **v17**.
+ exchange_code?: number;
+
+ // Access token needed to open the KYC SPA and/or
+ // access the /kyc-info/ endpoint.
+ access_token?: AccessToken;
+
+ // Array with limitations that currently apply to this
+ // account and that may be increased or lifted if the
+ // KYC check is passed.
+ // Note that additional limits *may* exist and not be
+ // communicated to the client. If such limits are
+ // reached, this *may* be indicated by the account
+ // going into aml_review state. However, it is
+ // also possible that the exchange may legally have
+ // to deny operations without being allowed to provide
+ // any justification.
+ // The limits should be used by the client to
+ // possibly structure their operations (e.g. withdraw
+ // what is possible below the limit, ask the user to
+ // pass KYC checks or withdraw the rest after the time
+ // limit is passed, warn the user to not withdraw too
+ // much or even prevent the user from generating a
+ // request that would cause it to exceed hard limits).
+ limits?: AccountLimit[];
+
+ // Array of wire transfer instructions (including
+ // optional amount and subject) for a KYC auth wire
+ // transfer. Set only if this is required
+ // to get the given exchange working.
+ // Array because the exchange may have multiple
+ // bank accounts, in which case any of these
+ // accounts will do.
+ // Optional. Since protocol **v17**.
+ payto_kycauths?: string[];
+}
+
+export 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;
+}
+
+export interface AccountAddDetails {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // 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;
+}
+
+export type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+
+export interface NoFacadeCredentials {
+ type: "none";
+}
+
+export interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+}
+
+export interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+}
+
+export 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".
+ // If the argument is omitted, the old credentials
+ // are simply preserved.
+ credit_facade_credentials?: FacadeCredentials;
+}
+
+export interface AccountsSummaryResponse {
+ // List of accounts that are known for the instance.
+ accounts: BankAccountEntry[];
+}
+
+// TODO: missing in docs
+export interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // true if this account is active,
+ // false if it is historic.
+ active?: boolean;
+}
+export interface BankAccountDetail {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // 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;
+
+ // true if this account is active,
+ // false if it is historic.
+ active?: boolean;
+}
+
+export interface CategoryListResponse {
+ // Array with all of the categories we know.
+ categories: CategoryListEntry[];
+}
+
+export interface CategoryListEntry {
+ // Unique number for the category.
+ category_id: Integer;
+
+ // Name of the category.
+ name: string;
+
+ // Translations of the name into various
+ // languages.
+ name_i18n?: { [lang_tag: string]: string };
+
+ // Number of products in this category.
+ // A product can be in more than one category.
+ product_count: Integer;
+}
+
+export interface CategoryProductList {
+ // Name of the category.
+ name: string;
+
+ // Translations of the name into various
+ // languages.
+ name_i18n?: { [lang_tag: string]: string };
+
+ // The products in this category.
+ products: ProductSummary[];
+}
+
+export interface ProductSummary {
+ // Product ID to use.
+ product_id: string;
+}
+
+export interface CategoryCreateRequest {
+ // Name of the category.
+ name: string;
+
+ // Translations of the name into various
+ // languages.
+ name_i18n?: { [lang_tag: string]: string };
+}
+
+export interface CategoryCreatedResponse {
+ // Number of the newly created category.
+ category_id: Integer;
+}
+
+export 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 };
+
+ // Categories into which the product belongs.
+ // Used in the POS-endpoint.
+ // Since API version **v16**.
+ categories?: Integer[];
+
+ // 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: AmountString;
+
+ // 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;
+}
+
+export interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Categories into which the product belongs.
+ // Used in the POS-endpoint.
+ // Since API version **v16**.
+ categories?: Integer[];
+
+ // 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: AmountString;
+
+ // 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;
+}
+
+export interface InventorySummaryResponse {
+ // List of products that are present in the inventory.
+ products: InventoryEntry[];
+}
+
+export interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ // product_serial_id of the product in the database.
+ product_serial: Integer;
+}
+
+export interface FullInventoryDetailsResponse {
+ // List of products that are present in the inventory.
+ products: MerchantPosProductDetail[];
+
+ // List of categories in the inventory.
+ categories: MerchantCategory[];
+}
+
+export interface MerchantPosProductDetail {
+ // A unique numeric ID of the product
+ product_serial: number;
+
+ // A merchant-internal unique identifier for the product
+ product_id?: string;
+
+ // A list of category IDs this product belongs to.
+ // Typically, a product only belongs to one category, but more than one is supported.
+ categories: number[];
+
+ // 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: AmountString;
+
+ // 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.
+ // Optional, if missing treat as "infinite".
+ total_stock?: Integer;
+
+ // Minimum age buyer must have (in years).
+ minimum_age?: Integer;
+}
+
+export interface MerchantCategory {
+ // A unique numeric ID of the category
+ id: number;
+
+ // The name of the category. This will be shown to users and used in the order summary.
+ name: string;
+
+ // Map from IETF BCP 47 language tags to localized names
+ name_i18n?: { [lang_tag: string]: string };
+}
+
+export 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;
+
+ // Categories into which the product belongs.
+ // Since API version **v16**.
+ categories: Integer[];
+
+ // 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: AmountString;
+
+ // 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).
+ minimum_age?: Integer;
+}
+export interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // Must be unique for the lifetime of the lock.
+ lock_uuid: string;
+
+ // How long does the frontend intend to hold the lock?
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+}
+
+export 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
+ // inventory_products is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping cart 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?: string[];
+
+ // 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;
+}
+
+export type Order = MinimalOrderDetail & Partial<ContractTerms>;
+
+export interface MinimalOrderDetail {
+ // Amount to be paid by the customer.
+ amount: AmountString;
+
+ // Short summary of the order.
+ summary: string;
+
+ // See documentation of fulfillment_url in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ // When creating an order, the fulfillment URL can
+ // contain ${ORDER_ID} which will be substituted with the
+ // order ID of the newly created order.
+ fulfillment_url?: string;
+
+ // See documentation of fulfillment_message in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+}
+
+export interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!).
+ product_id: string;
+
+ // How many units of the product are requested.
+ quantity: Integer;
+}
+
+export 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;
+}
+export interface OutOfStockResponse {
+ // Product ID of an out-of-stock item.
+ product_id: string;
+
+ // Requested quantity.
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quantity).
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+}
+
+export interface OrderHistory {
+ // Timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+}
+
+export 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: AmountString;
+
+ // 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;
+}
+
+export type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+
+export 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: AmountString;
+
+ // 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_code: 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_http_status: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: AmountString;
+
+ // 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;
+}
+
+export interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+}
+
+export interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // 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: AmountString;
+
+ // 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.
+}
+export interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: AmountString;
+}
+
+export interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32String;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: AmountString;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+}
+
+export interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_code: number;
+
+ // HTTP status code received from the exchange.
+ exchange_http_status: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+}
+
+export interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+}
+
+export interface RefundRequest {
+ // Amount to be refunded.
+ refund: AmountString;
+
+ // Human-readable refund justification.
+ reason: string;
+}
+export 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;
+}
+
+export interface TransferInformation {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+}
+
+export interface TransferList {
+ // List of all the transfers that fit the filter that we know.
+ transfers: TransferDetails[];
+}
+export interface TransferDetails {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filtering 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;
+}
+
+export interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ // "NONE" or 0: No algorithm (no pos confirmation will be generated)
+ // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device)
+ // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device)
+ // The "String" variants are supported @since protocol **v7**.
+ otp_algorithm: Integer | string;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+}
+
+export interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+}
+
+export interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+}
+export interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+}
+
+export interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ //
+ // Currently, the following numbers are defined:
+ // 0: None
+ // 1: TOTP without price
+ // 2: TOTP with price
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+
+ // Current time for time-based OTP devices.
+ // Will match the faketime argument of the
+ // query if one was present, otherwise the current
+ // time at the backend.
+ //
+ // Available since protocol **v10**.
+ otp_timestamp: Integer;
+
+ // Current OTP confirmation string of the device.
+ // Matches exactly the string that would be returned
+ // as part of a payment confirmation for the given
+ // amount and time (so may contain multiple OTP codes).
+ //
+ // If the otp_algorithm is time-based, the code is
+ // returned for the current time, or for the faketime
+ // if a TIMESTAMP query argument was provided by the client.
+ //
+ // When using OTP with counters, the counter is **NOT**
+ // increased merely because this endpoint created
+ // an OTP code (this is a GET request, after all!).
+ //
+ // If the otp_algorithm requires an amount, the
+ // amount argument must be specified in the
+ // query, otherwise the otp_code is not
+ // generated.
+ //
+ // This field is *optional* in the response, as it is
+ // only provided if we could compute it based on the
+ // otp_algorithm and matching client query arguments.
+ //
+ // Available since protocol **v10**.
+ otp_code?: string;
+}
+export 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;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+}
+export interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // Required currency for payments to the template.
+ // The user may specify any amount, but it must be
+ // in this currency.
+ // This parameter is optional and should not be present
+ // if "amount" is given.
+ currency?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: AmountString;
+
+ // 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;
+}
+
+export interface TemplateContractDetailsDefaults {
+ summary?: string;
+
+ currency?: string;
+
+ /**
+ * Amount *or* a plain currency string.
+ */
+ amount?: string;
+}
+
+export 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;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+}
+
+export interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+}
+
+export interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+}
+
+export interface WalletTemplateDetails {
+ // Hard-coded information about the contrac terms
+ // for this template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+}
+
+export 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;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+}
+export interface UsingTemplateDetails {
+ // Summary of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: AmountString;
+}
+
+export interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // 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;
+}
+
+export 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;
+}
+
+export interface WebhookSummaryResponse {
+ // Return webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+}
+
+export 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;
+}
+
+export 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;
+}
+
+export interface TokenFamilyCreateRequest {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ // If not specified, merchant backend will use the current time.
+ valid_after?: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+}
+
+export enum TokenFamilyKind {
+ Discount = "discount",
+ Subscription = "subscription",
+}
+
+export interface TokenFamilyUpdateRequest {
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+}
+
+export interface TokenFamiliesList {
+ // All configured token families of this instance.
+ token_families: TokenFamilySummary[];
+}
+
+export interface TokenFamilySummary {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+}
+
+export interface TokenFamilyDetails {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+
+ // How many tokens have been issued for this family.
+ issued: Integer;
+
+ // How many tokens have been redeemed for this family.
+ redeemed: Integer;
+}
+export 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: AmountString;
+
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
+ public_reorder_url?: 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).
+ // Note that this placeholder can only be used once.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_url?: string;
+
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
+ fulfillment_message_i18n?: { [lang_tag: string]: string };
+
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: AmountString;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated.
+ timestamp: Timestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: Timestamp;
+
+ // After this deadline, the merchant won't accept payments for the contract.
+ pay_deadline: Timestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: Timestamp;
+
+ // 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;
+
+ // 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?: Timestamp;
+
+ // 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 the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
+}
+
+export 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;
+
+ // 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?: AmountString;
+
+ // 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?: Timestamp;
+}
+
+export interface Tax {
+ // The name of the tax.
+ name: string;
+
+ // Amount paid in tax.
+ tax: AmountString;
+}
+
+export interface Merchant {
+ // The merchant's legal name of business.
+ name: string;
+
+ // Label for a location with the business address of the merchant.
+ email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // 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;
+}
+
+// Delivery location, loosely modeled as a subset of
+// ISO20022's PostalAddress25.
+export 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[];
+}
+
+export interface Exchange {
+ // The exchange's base URL.
+ url: string;
+
+ // How much would the merchant like to use this exchange.
+ // The wallet should use a suitable exchange with high
+ // priority. The following priority values are used, but
+ // it should be noted that they are NOT in any way normative.
+ //
+ // 0: likely it will not work (recently seen with account
+ // restriction that would be bad for this merchant)
+ // 512: merchant does not know, might be down (merchant
+ // did not yet get /wire response).
+ // 1024: good choice (recently confirmed working)
+ priority: Integer;
+
+ // Master public key of the exchange.
+ master_pub: EddsaPublicKey;
+}
+
+export interface MerchantReserveCreateConfirmation {
+ // Public key identifying the reserve.
+ reserve_pub: EddsaPublicKey;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: ExchangeWireAccount[];
+}
+
+export interface TemplateEditableDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // Required currency for payments to the template.
+ // The user may specify any amount, but it must be
+ // in this currency.
+ // This parameter is optional and should not be present
+ // if "amount" is given.
+ currency?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: AmountString;
+}
+
+export interface MerchantTemplateContractDetails {
+ // 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?: string;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: number;
+
+ // 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: TalerProtocolDuration;
+}
+
+export interface MerchantTemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // A base64-encoded image selected by the merchant.
+ // This parameter is optional.
+ // We are not sure about it.
+ image?: string;
+
+ editable_defaults?: TemplateEditableDetails;
+
+ // Additional information in a separate template.
+ template_contract: MerchantTemplateContractDetails;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+}
+
+const codecForExchangeConfigInfo = (): Codec<ExchangeConfigInfo> =>
+ buildCodecForObject<ExchangeConfigInfo>()
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .property("master_pub", codecForString())
+ .build("TalerMerchantApi.ExchangeConfigInfo");
+
+export const codecForTalerMerchantConfigResponse =
+ (): Codec<TalerMerchantConfigResponse> =>
+ buildCodecForObject<TalerMerchantConfigResponse>()
+ .property("name", codecForConstString("taler-merchant"))
+ .property("currency", codecForString())
+ .property("version", codecForString())
+ .property("currencies", codecForMap(codecForCurrencySpecificiation()))
+ .property("exchanges", codecForList(codecForExchangeConfigInfo()))
+ .build("TalerMerchantApi.VersionResponse");
+
+export const codecForClaimResponse = (): Codec<ClaimResponse> =>
+ buildCodecForObject<ClaimResponse>()
+ .property("contract_terms", codecForContractTerms())
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.ClaimResponse");
+
+export const codecForPaymentResponse = (): Codec<PaymentResponse> =>
+ buildCodecForObject<PaymentResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForPaymentDeniedLegallyResponse =
+ (): Codec<PaymentDeniedLegallyResponse> =>
+ buildCodecForObject<PaymentDeniedLegallyResponse>()
+ .property("exchange_base_urls", codecForList(codecForString()))
+ .build("TalerMerchantApi.PaymentDeniedLegallyResponse");
+
+export const codecForStatusPaid = (): Codec<StatusPaid> =>
+ buildCodecForObject<StatusPaid>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_pending", codecForBoolean())
+ .property("refund_taken", codecForAmountString())
+ .property("refunded", codecForBoolean())
+ .property("type", codecForConstString("paid"))
+ .build("TalerMerchantApi.StatusPaid");
+
+export const codecForStatusGoto = (): Codec<StatusGotoResponse> =>
+ buildCodecForObject<StatusGotoResponse>()
+ .property("public_reorder_url", codecForURLString())
+ .property("type", codecForConstString("goto"))
+ .build("TalerMerchantApi.StatusGotoResponse");
+
+export const codecForStatusStatusUnpaid = (): Codec<StatusUnpaidResponse> =>
+ buildCodecForObject<StatusUnpaidResponse>()
+ .property("type", codecForConstString("unpaid"))
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForPaidRefundStatusResponse =
+ (): Codec<PaidRefundStatusResponse> =>
+ buildCodecForObject<PaidRefundStatusResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("refunded", codecForBoolean())
+ .build("TalerMerchantApi.PaidRefundStatusResponse");
+
+export const codecForMerchantAbortPayRefundSuccessStatus =
+ (): Codec<MerchantAbortPayRefundSuccessStatus> =>
+ buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("type", codecForConstString("success"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus");
+
+export const codecForMerchantAbortPayRefundFailureStatus =
+ (): Codec<MerchantAbortPayRefundFailureStatus> =>
+ buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
+ .property("exchange_code", codecForNumber())
+ .property("exchange_reply", codecForAny())
+ .property("exchange_status", codecForNumber())
+ .property("type", codecForConstString("failure"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus");
+
+export const codecForMerchantAbortPayRefundStatus =
+ (): Codec<MerchantAbortPayRefundStatus> =>
+ buildCodecForUnion<MerchantAbortPayRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
+ .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantAbortPayRefundStatus");
+
+export const codecForAbortResponse = (): Codec<AbortResponse> =>
+ buildCodecForObject<AbortResponse>()
+ .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForWalletRefundResponse = (): Codec<WalletRefundResponse> =>
+ buildCodecForObject<WalletRefundResponse>()
+ .property("merchant_pub", codecForString())
+ .property("refund_amount", codecForAmountString())
+ .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus =
+ (): Codec<MerchantCoinRefundSuccessStatus> =>
+ buildCodecForObject<MerchantCoinRefundSuccessStatus>()
+ .property("type", codecForConstString("success"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("exchange_sig", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_pub", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus =
+ (): Codec<MerchantCoinRefundFailureStatus> =>
+ buildCodecForObject<MerchantCoinRefundFailureStatus>()
+ .property("type", codecForConstString("failure"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForNumber())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundFailureStatus");
+
+export const codecForMerchantCoinRefundStatus =
+ (): Codec<MerchantCoinRefundStatus> =>
+ buildCodecForUnion<MerchantCoinRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantCoinRefundSuccessStatus())
+ .alternative("failure", codecForMerchantCoinRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantCoinRefundStatus");
+
+export const codecForQueryInstancesResponse =
+ (): Codec<QueryInstancesResponse> =>
+ buildCodecForObject<QueryInstancesResponse>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("merchant_pub", codecForString())
+ .property("address", codecForLocation())
+ .property("jurisdiction", codecForLocation())
+ .property("use_stefan", codecForBoolean())
+ .property("default_wire_transfer_delay", codecForDuration)
+ .property("default_pay_delay", codecForDuration)
+ .property(
+ "auth",
+ buildCodecForObject<{
+ method: "external" | "token";
+ }>()
+ .property(
+ "method",
+ codecForEither(
+ codecForConstString("token"),
+ codecForConstString("external"),
+ ),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse.auth"),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse");
+
+export const codecForAccountKycRedirects =
+ (): Codec<MerchantAccountKycRedirectsResponse> =>
+ buildCodecForObject<MerchantAccountKycRedirectsResponse>()
+ .property("kyc_data", codecForList(codecForMerchantAccountKycRedirect()))
+
+ .build("TalerMerchantApi.MerchantAccountKycRedirectsResponse");
+
+export const codecForMerchantAccountKycRedirect =
+ (): Codec<MerchantAccountKycRedirect> =>
+ buildCodecForObject<MerchantAccountKycRedirect>()
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURLString())
+ .property("exchange_http_status", codecForNumber())
+ .property("no_keys", codecForBoolean())
+ .property("auth_conflict", codecForBoolean())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("access_token", codecOptional(codecForAccessToken()))
+ .property("limits", codecOptional(codecForList(codecForAccountLimit())))
+ .property("payto_kycauths", codecOptional(codecForList(codecForString())))
+ .build("TalerMerchantApi.MerchantAccountKycRedirect");
+
+export const codecForExchangeKycTimeout = (): Codec<ExchangeKycTimeout> =>
+ buildCodecForObject<ExchangeKycTimeout>()
+ .property("exchange_url", codecForURLString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .build("TalerMerchantApi.ExchangeKycTimeout");
+
+export const codecForAccountAddResponse = (): Codec<AccountAddResponse> =>
+ buildCodecForObject<AccountAddResponse>()
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .build("TalerMerchantApi.AccountAddResponse");
+
+export const codecForAccountsSummaryResponse =
+ (): Codec<AccountsSummaryResponse> =>
+ buildCodecForObject<AccountsSummaryResponse>()
+ .property("accounts", codecForList(codecForBankAccountEntry()))
+ .build("TalerMerchantApi.AccountsSummaryResponse");
+
+export const codecForBankAccountEntry = (): Codec<BankAccountEntry> =>
+ buildCodecForObject<BankAccountEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .property("active", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.BankAccountEntry");
+
+export const codecForBankAccountDetail = (): Codec<BankAccountDetail> =>
+ buildCodecForObject<BankAccountDetail>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .property("credit_facade_url", codecOptional(codecForURLString()))
+ .property("active", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.BankAccountEntry");
+
+export const codecForCategoryListResponse = (): Codec<CategoryListResponse> =>
+ buildCodecForObject<CategoryListResponse>()
+ .property("categories", codecForList(codecForCategoryListEntry()))
+ .build("TalerMerchantApi.CategoryListResponse");
+
+export const codecForCategoryListEntry = (): Codec<CategoryListEntry> =>
+ buildCodecForObject<CategoryListEntry>()
+ .property("category_id", codecForNumber())
+ .property("name", codecForString())
+ .property("name_i18n", codecForInternationalizedString())
+ .property("product_count", codecForNumber())
+ .build("TalerMerchantApi.CategoryListEntry");
+
+export const codecForCategoryProductList = (): Codec<CategoryProductList> =>
+ buildCodecForObject<CategoryProductList>()
+ .property("name", codecForString())
+ .property("name_i18n", codecForInternationalizedString())
+ .property("products", codecForList(codecForProductSummary()))
+ .build("TalerMerchantApi.CategoryProductList");
+
+export const codecForProductSummary = (): Codec<ProductSummary> =>
+ buildCodecForObject<ProductSummary>()
+ .property("product_id", codecForString())
+ // .property("description", codecForString())
+ // .property("description_i18n", codecForInternationalizedString())
+ .build("TalerMerchantApi.ProductSummary");
+
+export const codecForInventorySummaryResponse =
+ (): Codec<InventorySummaryResponse> =>
+ buildCodecForObject<InventorySummaryResponse>()
+ .property("products", codecForList(codecForInventoryEntry()))
+ .build("TalerMerchantApi.InventorySummaryResponse");
+
+export const codecForInventoryEntry = (): Codec<InventoryEntry> =>
+ buildCodecForObject<InventoryEntry>()
+ .property("product_id", codecForString())
+ .property("product_serial", codecForNumber())
+ .build("TalerMerchantApi.InventoryEntry");
+
+export const codecForMerchantPosProductDetail =
+ (): Codec<MerchantPosProductDetail> =>
+ buildCodecForObject<MerchantPosProductDetail>()
+ .property("product_serial", codecForNumber())
+ .property("product_id", codecOptional(codecForString()))
+ .property("categories", codecForList(codecForNumber()))
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("unit", codecForString())
+ .property("price", codecForAmountString())
+ .property("image", codecForString())
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("total_stock", codecForNumber())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.MerchantPosProductDetail");
+
+export const codecForMerchantCategory = (): Codec<MerchantCategory> =>
+ buildCodecForObject<MerchantCategory>()
+ .property("id", codecForNumber())
+ .property("name", codecForString())
+ .property("name_i18n", codecForInternationalizedString())
+ .build("TalerMerchantApi.MerchantCategory");
+
+export const codecForFullInventoryDetailsResponse =
+ (): Codec<FullInventoryDetailsResponse> =>
+ buildCodecForObject<FullInventoryDetailsResponse>()
+ .property("categories", codecForList(codecForMerchantCategory()))
+ .property("products", codecForList(codecForMerchantPosProductDetail()))
+ .build("TalerMerchantApi.FullInventoryDetailsResponse");
+
+export const codecForProductDetail = (): Codec<ProductDetail> =>
+ buildCodecForObject<ProductDetail>()
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("unit", codecForString())
+ .property("price", codecForAmountString())
+ .property("image", codecForString())
+ .property("categories", codecForList(codecForNumber()))
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("address", codecOptional(codecForLocation()))
+ .property("next_restock", codecOptional(codecForTimestamp))
+ .property("total_stock", codecForNumber())
+ .property("total_sold", codecForNumber())
+ .property("total_lost", codecForNumber())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.ProductDetail");
+
+export const codecForTax = (): Codec<Tax> =>
+ buildCodecForObject<Tax>()
+ .property("name", codecForString())
+ .property("tax", codecForAmountString())
+ .build("TalerMerchantApi.Tax");
+
+export const codecForPostOrderResponse = (): Codec<PostOrderResponse> =>
+ buildCodecForObject<PostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("TalerMerchantApi.PostOrderResponse");
+
+export const codecForOutOfStockResponse = (): Codec<OutOfStockResponse> =>
+ buildCodecForObject<OutOfStockResponse>()
+ .property("product_id", codecForString())
+ .property("available_quantity", codecForNumber())
+ .property("requested_quantity", codecForNumber())
+ .property("restock_expected", codecForTimestamp)
+ .build("TalerMerchantApi.OutOfStockResponse");
+
+export const codecForOrderHistory = (): Codec<OrderHistory> =>
+ buildCodecForObject<OrderHistory>()
+ .property("orders", codecForList(codecForOrderHistoryEntry()))
+ .build("TalerMerchantApi.OrderHistory");
+
+export const codecForOrderHistoryEntry = (): Codec<OrderHistoryEntry> =>
+ buildCodecForObject<OrderHistoryEntry>()
+ .property("order_id", codecForString())
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("summary", codecForString())
+ .property("refundable", codecForBoolean())
+ .property("paid", codecForBoolean())
+ .build("TalerMerchantApi.OrderHistoryEntry");
+
+export const codecForMerchant = (): Codec<Merchant> =>
+ buildCodecForObject<Merchant>()
+ .property("name", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("address", codecOptional(codecForLocation()))
+ .property("jurisdiction", codecOptional(codecForLocation()))
+ .build("TalerMerchantApi.MerchantInfo");
+
+export const codecForExchange = (): Codec<Exchange> =>
+ buildCodecForObject<Exchange>()
+ .property("master_pub", codecForString())
+ .property("priority", codecForNumber())
+ .property("url", codecForString())
+ .build("TalerMerchantApi.Exchange");
+
+export const codecForContractTerms = (): Codec<ContractTerms> =>
+ buildCodecForObject<ContractTerms>()
+ .property("order_id", codecForString())
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("fulfillment_message", codecOptional(codecForString()))
+ .property(
+ "fulfillment_message_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("merchant_base_url", codecForString())
+ .property("h_wire", codecForString())
+ .property("auto_refund", codecOptional(codecForDuration))
+ .property("wire_method", codecForString())
+ .property("summary", codecForString())
+ .property("summary_i18n", codecOptional(codecForInternationalizedString()))
+ .property("nonce", codecForString())
+ .property("amount", codecForAmountString())
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("delivery_location", codecOptional(codecForLocation()))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .property("max_fee", codecForAmountString())
+ .property("merchant", codecForMerchant())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchange()))
+ .property("products", codecForList(codecForProduct()))
+ .property("extra", codecForAny())
+ .build("TalerMerchantApi.ContractTerms");
+
+export const codecForProduct = (): Codec<Product> =>
+ buildCodecForObject<Product>()
+ .property("product_id", codecOptional(codecForString()))
+ .property("description", codecForString())
+ .property(
+ "description_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("quantity", codecOptional(codecForNumber()))
+ .property("unit", codecOptional(codecForString()))
+ .property("price", codecOptional(codecForAmountString()))
+ .property("image", codecOptional(codecForString()))
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .build("TalerMerchantApi.Product");
+
+export const codecForCheckPaymentPaidResponse =
+ (): Codec<CheckPaymentPaidResponse> =>
+ buildCodecForObject<CheckPaymentPaidResponse>()
+ .property("order_status", codecForConstString("paid"))
+ .property("refunded", codecForBoolean())
+ .property("refund_pending", codecForBoolean())
+ .property("wired", codecForBoolean())
+ .property("deposit_total", codecForAmountString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("contract_terms", codecForContractTerms())
+ .property("wire_reports", codecForList(codecForTransactionWireReport()))
+ .property("wire_details", codecForList(codecForTransactionWireTransfer()))
+ .property("refund_details", codecForList(codecForRefundDetails()))
+ .property("order_status_url", codecForURLString())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentUnpaidResponse =
+ (): Codec<CheckPaymentUnpaidResponse> =>
+ buildCodecForObject<CheckPaymentUnpaidResponse>()
+ .property("order_status", codecForConstString("unpaid"))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .property("creation_time", codecForTimestamp)
+ .property("summary", codecForString())
+ .property("total_amount", codecForAmountString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("already_paid_fulfillment_url", codecOptional(codecForString()))
+ .property("order_status_url", codecForString())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentClaimedResponse =
+ (): Codec<CheckPaymentClaimedResponse> =>
+ buildCodecForObject<CheckPaymentClaimedResponse>()
+ .property("order_status", codecForConstString("claimed"))
+ .property("contract_terms", codecForContractTerms())
+ .build("TalerMerchantApi.CheckPaymentClaimedResponse");
+
+export const codecForMerchantOrderPrivateStatusResponse =
+ (): Codec<MerchantOrderStatusResponse> =>
+ buildCodecForUnion<MerchantOrderStatusResponse>()
+ .discriminateOn("order_status")
+ .alternative("paid", codecForCheckPaymentPaidResponse())
+ .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
+ .alternative("claimed", codecForCheckPaymentClaimedResponse())
+ .build("TalerMerchantApi.MerchantOrderStatusResponse");
+
+export const codecForRefundDetails = (): Codec<RefundDetails> =>
+ buildCodecForObject<RefundDetails>()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .build("TalerMerchantApi.RefundDetails");
+
+export const codecForTransactionWireTransfer =
+ (): Codec<TransactionWireTransfer> =>
+ buildCodecForObject<TransactionWireTransfer>()
+ .property("exchange_url", codecForURLString())
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("confirmed", codecForBoolean())
+ .build("TalerMerchantApi.TransactionWireTransfer");
+
+export const codecForTransactionWireReport = (): Codec<TransactionWireReport> =>
+ buildCodecForObject<TransactionWireReport>()
+ .property("code", codecForNumber())
+ .property("hint", codecForString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("coin_pub", codecForString())
+ .build("TalerMerchantApi.TransactionWireReport");
+
+export const codecForMerchantRefundResponse =
+ (): Codec<MerchantRefundResponse> =>
+ buildCodecForObject<MerchantRefundResponse>()
+ .property("taler_refund_uri", codecForTalerUriString())
+ .property("h_contract", codecForString())
+ .build("TalerMerchantApi.MerchantRefundResponse");
+
+export const codecForTansferList = (): Codec<TransferList> =>
+ buildCodecForObject<TransferList>()
+ .property("transfers", codecForList(codecForTransferDetails()))
+ .build("TalerMerchantApi.TransferList");
+
+export const codecForTransferDetails = (): Codec<TransferDetails> =>
+ buildCodecForObject<TransferDetails>()
+ .property("credit_amount", codecForAmountString())
+ .property("wtid", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURLString())
+ .property("transfer_serial_id", codecForNumber())
+ .property("execution_time", codecOptional(codecForTimestamp))
+ .property("verified", codecOptional(codecForBoolean()))
+ .property("confirmed", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.TransferDetails");
+
+export const codecForOtpDeviceSummaryResponse =
+ (): Codec<OtpDeviceSummaryResponse> =>
+ buildCodecForObject<OtpDeviceSummaryResponse>()
+ .property("otp_devices", codecForList(codecForOtpDeviceEntry()))
+ .build("TalerMerchantApi.OtpDeviceSummaryResponse");
+
+export const codecForOtpDeviceEntry = (): Codec<OtpDeviceEntry> =>
+ buildCodecForObject<OtpDeviceEntry>()
+ .property("otp_device_id", codecForString())
+ .property("device_description", codecForString())
+ .build("TalerMerchantApi.OtpDeviceEntry");
+
+export const codecForOtpDeviceDetails = (): Codec<OtpDeviceDetails> =>
+ buildCodecForObject<OtpDeviceDetails>()
+ .property("device_description", codecForString())
+ .property("otp_algorithm", codecForNumber())
+ .property("otp_ctr", codecOptional(codecForNumber()))
+ .property("otp_timestamp", codecForNumber())
+ .property("otp_code", codecOptional(codecForString()))
+ .build("TalerMerchantApi.OtpDeviceDetails");
+
+export const codecForTemplateSummaryResponse =
+ (): Codec<TemplateSummaryResponse> =>
+ buildCodecForObject<TemplateSummaryResponse>()
+ .property("templates", codecForList(codecForTemplateEntry()))
+ .build("TalerMerchantApi.TemplateSummaryResponse");
+
+export const codecForTemplateEntry = (): Codec<TemplateEntry> =>
+ buildCodecForObject<TemplateEntry>()
+ .property("template_id", codecForString())
+ .property("template_description", codecForString())
+ .build("TalerMerchantApi.TemplateEntry");
+
+export const codecForTemplateDetails = (): Codec<TemplateDetails> =>
+ buildCodecForObject<TemplateDetails>()
+ .property("template_description", codecForString())
+ .property("otp_id", codecOptional(codecForString()))
+ .property("template_contract", codecForTemplateContractDetails())
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.TemplateDetails");
+
+export const codecForTemplateContractDetails =
+ (): Codec<TemplateContractDetails> =>
+ buildCodecForObject<TemplateContractDetails>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecForNumber())
+ .property("pay_duration", codecForDuration)
+ .build("TalerMerchantApi.TemplateContractDetails");
+
+export const codecForTemplateContractDetailsDefaults =
+ (): Codec<TemplateContractDetailsDefaults> =>
+ buildCodecForObject<TemplateContractDetailsDefaults>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .build("TalerMerchantApi.TemplateContractDetailsDefaults");
+
+export const codecForWalletTemplateDetails = (): Codec<WalletTemplateDetails> =>
+ buildCodecForObject<WalletTemplateDetails>()
+ .property("template_contract", codecForTemplateContractDetails())
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.WalletTemplateDetails");
+
+export const codecForWebhookSummaryResponse =
+ (): Codec<WebhookSummaryResponse> =>
+ buildCodecForObject<WebhookSummaryResponse>()
+ .property("webhooks", codecForList(codecForWebhookEntry()))
+ .build("TalerMerchantApi.WebhookSummaryResponse");
+
+export const codecForWebhookEntry = (): Codec<WebhookEntry> =>
+ buildCodecForObject<WebhookEntry>()
+ .property("webhook_id", codecForString())
+ .property("event_type", codecForString())
+ .build("TalerMerchantApi.WebhookEntry");
+
+export const codecForWebhookDetails = (): Codec<WebhookDetails> =>
+ buildCodecForObject<WebhookDetails>()
+ .property("event_type", codecForString())
+ .property("url", codecForString())
+ .property("http_method", codecForString())
+ .property("header_template", codecOptional(codecForString()))
+ .property("body_template", codecOptional(codecForString()))
+ .build("TalerMerchantApi.WebhookDetails");
+
+export const codecForTokenFamilyKind = (): Codec<TokenFamilyKind> =>
+ codecForEither(
+ codecForConstString("discount"),
+ codecForConstString("subscription"),
+ ) as any; //FIXME: create a codecForEnum
+export const codecForTokenFamilyDetails = (): Codec<TokenFamilyDetails> =>
+ buildCodecForObject<TokenFamilyDetails>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("duration", codecForDuration)
+ .property("kind", codecForTokenFamilyKind())
+ .property("issued", codecForNumber())
+ .property("redeemed", codecForNumber())
+ .build("TalerMerchantApi.TokenFamilyDetails");
+
+export const codecForTokenFamiliesList = (): Codec<TokenFamiliesList> =>
+ buildCodecForObject<TokenFamiliesList>()
+ .property("token_families", codecForList(codecForTokenFamilySummary()))
+ .build("TalerMerchantApi.TokenFamiliesList");
+
+export const codecForTokenFamilySummary = (): Codec<TokenFamilySummary> =>
+ buildCodecForObject<TokenFamilySummary>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("kind", codecForTokenFamilyKind())
+ .build("TalerMerchantApi.TokenFamilySummary");
+
+export const codecForInstancesResponse = (): Codec<InstancesResponse> =>
+ buildCodecForObject<InstancesResponse>()
+ .property("instances", codecForList(codecForInstance()))
+ .build("TalerMerchantApi.InstancesResponse");
+
+export const codecForInstance = (): Codec<Instance> =>
+ buildCodecForObject<Instance>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("id", codecForString())
+ .property("merchant_pub", codecForString())
+ .property("payment_targets", codecForList(codecForString()))
+ .property("deleted", codecForBoolean())
+ .build("TalerMerchantApi.Instance");
+
+export const codecForTemplateEditableDetails =
+ (): Codec<TemplateEditableDetails> =>
+ buildCodecForObject<TemplateEditableDetails>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .build("TemplateEditableDetails");
+
+export const codecForMerchantReserveCreateConfirmation =
+ (): Codec<MerchantReserveCreateConfirmation> =>
+ buildCodecForObject<MerchantReserveCreateConfirmation>()
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("reserve_pub", codecForString())
+ .build("MerchantReserveCreateConfirmation");
diff --git a/packages/taler-util/src/types-taler-revenue.ts b/packages/taler-util/src/types-taler-revenue.ts
new file mode 100644
index 000000000..772871d19
--- /dev/null
+++ b/packages/taler-util/src/types-taler-revenue.ts
@@ -0,0 +1,95 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { codecForAmountString } from "./amounts.js";
+import { Codec, buildCodecForObject, codecForConstString, codecForList, codecForNumber, codecForString, codecOptional } from "./codec.js";
+import { codecForPaytoString } from "./payto.js";
+import { codecForTimestamp } from "./time.js";
+import { AmountString, SafeUint64, Timestamp } from "./types-taler-common.js";
+
+export interface RevenueConfig {
+ // Name of the API.
+ name: "taler-revenue";
+
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this gateway.
+ currency: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+}
+
+export interface RevenueIncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: RevenueIncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+ credit_account: string;
+}
+
+export interface RevenueIncomingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: string;
+
+ // The wire transfer subject.
+ subject: string;
+}
+
+export const codecForRevenueConfig = (): Codec<RevenueConfig> =>
+ buildCodecForObject<RevenueConfig>()
+ .property("name", codecForConstString("taler-revenue"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("TalerRevenueApi.RevenueConfig");
+
+export const codecForRevenueIncomingHistory =
+ (): Codec<RevenueIncomingHistory> =>
+ buildCodecForObject<RevenueIncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForRevenueIncomingBankTransaction()),
+ )
+ .build("TalerRevenueApi.MerchantIncomingHistory");
+
+export const codecForRevenueIncomingBankTransaction =
+ (): Codec<RevenueIncomingBankTransaction> =>
+ buildCodecForObject<RevenueIncomingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("row_id", codecForNumber())
+ .property("subject", codecForString())
+ .build("TalerRevenueApi.RevenueIncomingBankTransaction");
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/types-taler-sync.ts
index 8c38b70a6..bb0f4958f 100644
--- a/packages/taler-util/src/backup-types.ts
+++ b/packages/taler-util/src/types-taler-sync.ts
@@ -1,20 +1,22 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (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
+ terms of the GNU Affero 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
+ You should have received a copy of the GNU Affero General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { AmountString } from "./taler-types.js";
+import { AmountString } from "./types-taler-common.js";
export interface BackupRecovery {
walletRootPriv: string;
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
index a6ac5aec6..226e4f924 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -1,17 +1,19 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
+ (C) 2019-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
+ terms of the GNU Affero 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
+ You should have received a copy of the GNU Affero General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
@@ -36,13 +38,16 @@ import {
codecOptional,
} from "./codec.js";
import {
+ TalerPreciseTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+} from "./time.js";
+import {
AmountString,
InternationalizedString,
- MerchantInfo,
codecForInternationalizedString,
- codecForMerchantInfo,
-} from "./taler-types.js";
-import { TalerPreciseTimestamp, TalerProtocolTimestamp } from "./time.js";
+} from "./types-taler-common.js";
+import { MerchantInfo, codecForMerchantInfo } from "./types-taler-merchant.js";
import {
RefreshReason,
ScopeInfo,
@@ -51,7 +56,7 @@ import {
TransactionStateFilter,
WithdrawalExchangeAccountDetails,
codecForScopeInfo,
-} from "./wallet-types.js";
+} from "./types-taler-wallet.js";
export interface TransactionsRequest {
/**
@@ -122,8 +127,10 @@ export enum TransactionMinorState {
Unknown = "unknown",
Deposit = "deposit",
KycRequired = "kyc",
- AmlRequired = "aml",
MergeKycRequired = "merge-kyc",
+ BalanceKycRequired = "balance-kyc",
+ BalanceKycInit = "balance-kyc-init",
+ KycAuthRequired = "kyc-auth",
Track = "track",
SubmitPayment = "submit-payment",
RebindSession = "rebind-session",
@@ -186,6 +193,11 @@ export interface TransactionCommon {
timestamp: TalerPreciseTimestamp;
/**
+ * Scope of this tx
+ */
+ scopes: ScopeInfo[];
+
+ /**
* Transaction state, as per DD37.
*/
txState: TransactionState;
@@ -207,11 +219,47 @@ export interface TransactionCommon {
error?: TalerErrorDetail;
+ abortReason?: TalerErrorDetail;
+
+ failReason?: TalerErrorDetail;
+
/**
* If the transaction minor state is in KycRequired this field is going to
* have the location where the user need to go to complete KYC information.
*/
kycUrl?: string;
+
+ /**
+ * KYC payto hash. Useful for testing, not so useful for UIs.
+ */
+ kycPaytoHash?: string;
+
+ /**
+ * KYC access token. Useful for testing, not so useful for UIs.
+ */
+ kycAccessToken?: string;
+
+ kycAuthTransferInfo?: KycAuthTransferInfo;
+}
+
+export interface KycAuthTransferInfo {
+ /**
+ * Payto URI of the account that must make the transfer.
+ *
+ * The KYC auth transfer will *not* work if it originates
+ * from a different account.
+ */
+ debitPaytoUri: string;
+
+ /**
+ * Account public key that must be included in the subject.
+ */
+ accountPub: string;
+
+ /**
+ * Possible target payto URIs.
+ */
+ creditPaytoUris: string[];
}
export type Transaction =
@@ -273,6 +321,12 @@ interface WithdrawalDetailsForManualTransfer {
* Is the reserve ready for withdrawal?
*/
reserveIsReady: boolean;
+
+ /**
+ * How long does the exchange wait to transfer back funds from a
+ * reserve?
+ */
+ reserveClosingDelay: TalerProtocolDuration;
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
@@ -299,6 +353,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
*/
reserveIsReady: boolean;
+ /**
+ * Is the bank transfer for the withdrawal externally confirmed?
+ */
+ externalConfirmation?: boolean;
+
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
@@ -734,16 +793,6 @@ export const codecForTransactionByIdRequest =
.property("transactionId", codecForString())
.build("TransactionByIdRequest");
-export interface WithdrawalTransactionByURIRequest {
- talerWithdrawUri: string;
-}
-
-export const codecForWithdrawalTransactionByURIRequest =
- (): Codec<WithdrawalTransactionByURIRequest> =>
- buildCodecForObject<WithdrawalTransactionByURIRequest>()
- .property("talerWithdrawUri", codecForString())
- .build("WithdrawalTransactionByURIRequest");
-
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString()))
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/types-taler-wallet.ts
index a7aa4f863..9d73c1f70 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/types-taler-wallet.ts
@@ -1,17 +1,19 @@
/*
This file is part of GNU Taler
- (C) 2015-2020 Taler Systems SA
+ (C) 2019-2024 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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
+ 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
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
@@ -28,7 +30,6 @@
* Imports.
*/
import { AmountJson, codecForAmountString } from "./amounts.js";
-import { BackupRecovery } from "./backup-types.js";
import {
Codec,
Context,
@@ -47,49 +48,51 @@ import {
renderContext,
} from "./codec.js";
import {
+ AmountString,
CurrencySpecification,
+ EddsaPrivateKeyString,
+ InternationalizedString,
TalerMerchantApi,
TemplateParams,
WithdrawalOperationStatus,
canonicalizeBaseUrl,
} from "./index.js";
-import { VersionMatchResult } from "./libtool-version.js";
import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js";
+import { QrCodeSpec } from "./qr.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
+ AbsoluteTime,
+ TalerPreciseTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ codecForPreciseTimestamp,
+ codecForTimestamp,
+} from "./time.js";
+import {
AccountRestriction,
- AmountString,
AuditorDenomSig,
CoinEnvelope,
DenomKeyType,
DenominationPubKey,
- EddsaPrivateKeyString,
ExchangeAuditor,
ExchangeWireAccount,
- InternationalizedString,
- MerchantContractTerms,
- MerchantInfo,
PeerContractTerms,
UnblindedSignature,
codecForExchangeWireAccount,
- codecForMerchantContractTerms,
codecForPeerContractTerms,
-} from "./taler-types.js";
+} from "./types-taler-exchange.js";
import {
- AbsoluteTime,
- TalerPreciseTimestamp,
- TalerProtocolDuration,
- TalerProtocolTimestamp,
- codecForAbsoluteTime,
- codecForPreciseTimestamp,
- codecForTimestamp,
-} from "./time.js";
+ MerchantContractTerms,
+ MerchantInfo,
+ codecForMerchantContractTerms,
+} from "./types-taler-merchant.js";
+import { BackupRecovery } from "./types-taler-sync.js";
import {
OrderShortInfo,
TransactionState,
- TransactionType,
-} from "./transactions-types.js";
+} from "./types-taler-wallet-transactions.js";
/**
* Identifier for a transaction in the wallet.
@@ -172,6 +175,14 @@ export function codecForCanonBaseUrl(): Codec<string> {
};
}
+export const codecForScopeInfo = (): Codec<ScopeInfo> =>
+ buildCodecForUnion<ScopeInfo>()
+ .discriminateOn("type")
+ .alternative(ScopeType.Global, codecForScopeInfoGlobal())
+ .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
+ .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
+ .build("ScopeInfo");
+
/**
* Response for the create reserve request to the wallet.
*/
@@ -210,22 +221,6 @@ export enum TransactionAmountMode {
Raw = "raw",
}
-export type GetPlanForOperationRequest =
- | GetPlanForWithdrawRequest
- | GetPlanForDepositRequest;
-// | GetPlanForPushDebitRequest
-// | GetPlanForPullCreditRequest
-// | GetPlanForPaymentRequest
-// | GetPlanForTipRequest
-// | GetPlanForRefundRequest
-// | GetPlanForPullDebitRequest
-// | GetPlanForPushCreditRequest;
-
-interface GetPlanForWalletInitiatedOperation {
- instructedAmount: AmountString;
- mode: TransactionAmountMode;
-}
-
export interface ConvertAmountRequest {
amount: AmountString;
type: TransactionAmountMode;
@@ -245,148 +240,45 @@ export const codecForConvertAmountRequest =
)
.build("ConvertAmountRequest");
-export interface GetAmountRequest {
+export interface GetMaxDepositAmountRequest {
currency: string;
+ depositPaytoUri?: string;
}
-export const codecForGetAmountRequest = buildCodecForObject<GetAmountRequest>()
- .property("currency", codecForString())
- .build("GetAmountRequest");
-
-interface GetPlanToCompleteOperation {
- instructedAmount: AmountString;
-}
-
-const codecForGetPlanForWalletInitiatedOperation = <
- T extends GetPlanForWalletInitiatedOperation,
->() =>
- buildCodecForObject<T>()
- .property(
- "mode",
- codecForEither(
- codecForConstString(TransactionAmountMode.Raw),
- codecForConstString(TransactionAmountMode.Effective),
- ),
- )
- .property("instructedAmount", codecForAmountString());
-
-interface GetPlanForWithdrawRequest extends GetPlanForWalletInitiatedOperation {
- type: TransactionType.Withdrawal;
- exchangeUrl?: string;
-}
-interface GetPlanForDepositRequest extends GetPlanForWalletInitiatedOperation {
- type: TransactionType.Deposit;
- account: string; //payto string
-}
-interface GetPlanForPushDebitRequest
- extends GetPlanForWalletInitiatedOperation {
- type: TransactionType.PeerPushDebit;
-}
-
-interface GetPlanForPullCreditRequest
- extends GetPlanForWalletInitiatedOperation {
- type: TransactionType.PeerPullCredit;
- exchangeUrl: string;
-}
-
-const codecForGetPlanForWithdrawRequest =
- codecForGetPlanForWalletInitiatedOperation<GetPlanForWithdrawRequest>()
- .property("type", codecForConstString(TransactionType.Withdrawal))
- .property("exchangeUrl", codecOptional(codecForString()))
- .build("GetPlanForWithdrawRequest");
-
-const codecForGetPlanForDepositRequest =
- codecForGetPlanForWalletInitiatedOperation<GetPlanForDepositRequest>()
- .property("type", codecForConstString(TransactionType.Deposit))
- .property("account", codecForString())
- .build("GetPlanForDepositRequest");
-
-const codecForGetPlanForPushDebitRequest =
- codecForGetPlanForWalletInitiatedOperation<GetPlanForPushDebitRequest>()
- .property("type", codecForConstString(TransactionType.PeerPushDebit))
- .build("GetPlanForPushDebitRequest");
-
-const codecForGetPlanForPullCreditRequest =
- codecForGetPlanForWalletInitiatedOperation<GetPlanForPullCreditRequest>()
- .property("type", codecForConstString(TransactionType.PeerPullCredit))
- .property("exchangeUrl", codecForString())
- .build("GetPlanForPullCreditRequest");
+export const codecForGetMaxDepositAmountRequest =
+ buildCodecForObject<GetMaxDepositAmountRequest>()
+ .property("currency", codecForString())
+ .property("depositPaytoUri", codecOptional(codecForString()))
+ .build("GetAmountRequest");
-interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
- type: TransactionType.Payment;
- wireMethod: string;
- ageRestriction: number;
- maxDepositFee: AmountString;
+export interface GetMaxPeerPushDebitAmountRequest {
+ currency: string;
+ /**
+ * Preferred exchange to use for the p2p payment.
+ */
+ exchangeBaseUrl?: string;
+ restrictScope?: ScopeInfo;
}
-interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
- type: TransactionType.PeerPullDebit;
-}
+export const codecForGetMaxPeerPushDebitAmountRequest =
+ (): Codec<GetMaxPeerPushDebitAmountRequest> =>
+ buildCodecForObject<GetMaxPeerPushDebitAmountRequest>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .property("restrictScope", codecOptional(codecForScopeInfo()))
+ .build("GetMaxPeerPushDebitRequest");
-interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
- type: TransactionType.PeerPushCredit;
+export interface GetMaxDepositAmountResponse {
+ effectiveAmount: AmountString;
+ rawAmount: AmountString;
}
-const codecForGetPlanForPaymentRequest =
- buildCodecForObject<GetPlanForPaymentRequest>()
- .property("type", codecForConstString(TransactionType.Payment))
- .property("maxDepositFee", codecForAmountString())
- .build("GetPlanForPaymentRequest");
-
-const codecForGetPlanForPullDebitRequest =
- buildCodecForObject<GetPlanForPullDebitRequest>()
- .property("type", codecForConstString(TransactionType.PeerPullDebit))
- .build("GetPlanForPullDebitRequest");
-
-const codecForGetPlanForPushCreditRequest =
- buildCodecForObject<GetPlanForPushCreditRequest>()
- .property("type", codecForConstString(TransactionType.PeerPushCredit))
- .build("GetPlanForPushCreditRequest");
-
-export const codecForGetPlanForOperationRequest =
- (): Codec<GetPlanForOperationRequest> =>
- buildCodecForUnion<GetPlanForOperationRequest>()
- .discriminateOn("type")
- .alternative(
- TransactionType.Withdrawal,
- codecForGetPlanForWithdrawRequest,
- )
- .alternative(TransactionType.Deposit, codecForGetPlanForDepositRequest)
- // .alternative(
- // TransactionType.PeerPushDebit,
- // codecForGetPlanForPushDebitRequest,
- // )
- // .alternative(
- // TransactionType.PeerPullCredit,
- // codecForGetPlanForPullCreditRequest,
- // )
- // .alternative(TransactionType.Payment, codecForGetPlanForPaymentRequest)
- // .alternative(
- // TransactionType.PeerPullDebit,
- // codecForGetPlanForPullDebitRequest,
- // )
- // .alternative(
- // TransactionType.PeerPushCredit,
- // codecForGetPlanForPushCreditRequest,
- // )
- .build("GetPlanForOperationRequest");
-
-export interface GetPlanForOperationResponse {
+export interface GetMaxPeerPushDebitAmountResponse {
effectiveAmount: AmountString;
rawAmount: AmountString;
- counterPartyAmount?: AmountString;
- details: any;
+ exchangeBaseUrl?: string;
}
-export const codecForGetPlanForOperationResponse =
- (): Codec<GetPlanForOperationResponse> =>
- buildCodecForObject<GetPlanForOperationResponse>()
- .property("effectiveAmount", codecForAmountString())
- .property("rawAmount", codecForAmountString())
- .property("details", codecForAny())
- .property("counterPartyAmount", codecOptional(codecForAmountString()))
- .build("GetPlanForOperationResponse");
-
export interface AmountResponse {
effectiveAmount: AmountString;
rawAmount: AmountString;
@@ -449,14 +341,6 @@ export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
.property("url", codecForString())
.build("ScopeInfoAuditor");
-export const codecForScopeInfo = (): Codec<ScopeInfo> =>
- buildCodecForUnion<ScopeInfo>()
- .discriminateOn("type")
- .alternative(ScopeType.Global, codecForScopeInfoGlobal())
- .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
- .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
- .build("ScopeInfo");
-
export interface GetCurrencySpecificationRequest {
scope: ScopeInfo;
}
@@ -557,11 +441,13 @@ export enum ScopeType {
}
export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
+
export type ScopeInfoExchange = {
type: ScopeType.Exchange;
currency: string;
url: string;
};
+
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
@@ -570,6 +456,63 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+/**
+ * Shorter version of stringifyScopeInfo
+ *
+ * Format must be stable as it's used in the database.
+ */
+export function stringifyScopeInfoShort(si: ScopeInfo): string {
+ switch (si.type) {
+ case ScopeType.Global:
+ return `${si.currency}`;
+ case ScopeType.Exchange:
+ return `${si.currency}/${encodeURIComponent(si.url)}`;
+ case ScopeType.Auditor:
+ return `${si.currency}:${encodeURIComponent(si.url)}`;
+ }
+}
+export function parseScopeInfoShort(si: string): ScopeInfo | undefined {
+ const indexOfColon = si.indexOf(":");
+ const indexOfSlash = si.indexOf("/");
+ if (indexOfColon === -1 && indexOfSlash === -1) {
+ return {
+ type: ScopeType.Global,
+ currency: si,
+ };
+ }
+ if (indexOfColon > 0) {
+ return {
+ type: ScopeType.Auditor,
+ currency: si.substring(0, indexOfColon),
+ url: decodeURIComponent(si.substring(indexOfColon + 1)),
+ };
+ }
+ if (indexOfSlash > 0) {
+ return {
+ type: ScopeType.Exchange,
+ currency: si.substring(0, indexOfSlash),
+ url: decodeURIComponent(si.substring(indexOfSlash + 1)),
+ };
+ }
+ return undefined;
+}
+
+/**
+ * Encode scope info as a string.
+ *
+ * Format must be stable as it's used in the database.
+ */
+export function stringifyScopeInfo(si: ScopeInfo): string {
+ switch (si.type) {
+ case ScopeType.Global:
+ return `taler-si:global/${si.currency}`;
+ case ScopeType.Auditor:
+ return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
+ case ScopeType.Exchange:
+ return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
+ }
+}
+
export interface BalancesResponse {
balances: WalletBalance[];
}
@@ -627,6 +570,32 @@ export enum CoinStatus {
Dormant = "dormant",
}
+export type WalletCoinHistoryItem =
+ | {
+ type: "withdraw";
+ transactionId: TransactionIdStr;
+ }
+ | {
+ type: "spend";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "refresh";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "recoup";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "refund";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ };
+
/**
* Easy to process format for the public data of coins
* managed by the wallet.
@@ -636,44 +605,42 @@ export interface CoinDumpJson {
/**
* The coin's denomination's public key.
*/
- denom_pub: DenominationPubKey;
+ denomPub: DenominationPubKey;
/**
* Hash of denom_pub.
*/
- denom_pub_hash: string;
+ denomPubHash: string;
/**
* Value of the denomination (without any fees).
*/
- denom_value: string;
+ denomValue: string;
/**
* Public key of the coin.
*/
- coin_pub: string;
+ coinPub: string;
/**
* Base URL of the exchange for the coin.
*/
- exchange_base_url: string;
+ exchangeBaseUrl: string;
/**
* Public key of the parent coin.
* Only present if this coin was obtained via refreshing.
*/
- refresh_parent_coin_pub: string | undefined;
+ refreshParentCoinPub: string | undefined;
/**
* Public key of the reserve for this coin.
* Only present if this coin was obtained via refreshing.
*/
- withdrawal_reserve_pub: string | undefined;
- coin_status: CoinStatus;
- spend_allocation:
- | {
- id: string;
- amount: AmountString;
- }
- | undefined;
+ withdrawalReservePub: string | undefined;
+ /**
+ * Status of the coin.
+ */
+ coinStatus: CoinStatus;
/**
* Information about the age restriction
*/
ageCommitmentProof: AgeCommitmentProof | undefined;
+ history: WalletCoinHistoryItem[];
}>;
}
@@ -1338,6 +1305,7 @@ export enum ExchangeTosStatus {
Pending = "pending",
Proposed = "proposed",
Accepted = "accepted",
+ MissingTos = "missing-tos",
}
export enum ExchangeEntryStatus {
@@ -1353,6 +1321,19 @@ export enum ExchangeUpdateStatus {
UnavailableUpdate = "unavailable-update",
Ready = "ready",
ReadyUpdate = "ready-update",
+ OutdatedUpdate = "outdated-update",
+}
+
+export enum ExchangeWalletKycStatus {
+ Done = "done",
+ /**
+ * Wallet needs to request KYC status.
+ */
+ LegiInit = "legi-init",
+ /**
+ * User requires KYC or AML.
+ */
+ Legi = "legi",
}
export interface OperationErrorInfo {
@@ -1376,6 +1357,11 @@ export interface ExchangeListItem {
exchangeUpdateStatus: ExchangeUpdateStatus;
ageRestrictionOptions: number[];
+ walletKycStatus?: ExchangeWalletKycStatus;
+ walletKycReservePub?: string;
+ walletKycAccessToken?: string;
+ walletKycUrl?: string;
+
/**
* P2P payments are disabled with this exchange
* (e.g. because no global fees are configured).
@@ -1396,6 +1382,8 @@ export interface ExchangeListItem {
* to update the exchange info.
*/
lastUpdateErrorInfo?: OperationErrorInfo;
+
+ unavailableReason?: TalerErrorDetail;
}
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@@ -1541,6 +1529,20 @@ export interface WithdrawalDetailsForAmount {
* Scope info of the currency withdrawn.
*/
scopeInfo: ScopeInfo;
+
+ /**
+ * KYC soft limit.
+ *
+ * Withdrawals over that amount will require KYC.
+ */
+ kycSoftLimit?: AmountString;
+
+ /**
+ * KYC soft limits.
+ *
+ * Withdrawals over that amount will be denied.
+ */
+ kycHardLimit?: AmountString;
}
export interface DenomSelItem {
@@ -1554,13 +1556,12 @@ export interface DenomSelItem {
}
/**
- * Selected denominations withn some extra info.
+ * Selected denominations with some extra info.
*/
export interface DenomSelectionState {
totalCoinValue: AmountString;
totalWithdrawCost: AmountString;
selectedDenoms: DenomSelItem[];
- earliestDepositExpiration: TalerProtocolTimestamp;
hasDenomWithAgeRestriction: boolean;
}
@@ -1590,30 +1591,6 @@ export interface ExchangeWithdrawalDetails {
termsOfServiceAccepted: boolean;
/**
- * The earliest deposit expiration of the selected coins.
- */
- earliestDepositExpiration: TalerProtocolTimestamp;
-
- /**
- * Result of checking the wallet's version
- * against the exchange's version.
- *
- * Older exchanges don't return version information.
- */
- versionMatch: VersionMatchResult | undefined;
-
- /**
- * Libtool-style version string for the exchange or "unknown"
- * for older exchanges.
- */
- exchangeVersion: string;
-
- /**
- * Libtool-style version string for the wallet.
- */
- walletVersion: string;
-
- /**
* Amount that will be subtracted from the reserve's balance.
*/
withdrawalAmountRaw: AmountString;
@@ -1631,6 +1608,20 @@ export interface ExchangeWithdrawalDetails {
ageRestrictionOptions?: number[];
scopeInfo: ScopeInfo;
+
+ /**
+ * KYC soft limit.
+ *
+ * Withdrawals over that amount will require KYC.
+ */
+ kycSoftLimit?: AmountString;
+
+ /**
+ * KYC soft limits.
+ *
+ * Withdrawals over that amount will be denied.
+ */
+ kycHardLimit?: AmountString;
}
export interface GetExchangeTosResult {
@@ -2328,17 +2319,42 @@ export interface CreateDepositGroupRequest {
amount: AmountString;
}
-export interface PrepareDepositRequest {
+export interface CheckDepositRequest {
+ /**
+ * Payto URI to identify the (bank) account that the exchange will wire
+ * the money to.
+ */
depositPaytoUri: string;
+
+ /**
+ * Amount that should be deposited.
+ *
+ * Raw amount, fees will be added on top.
+ */
amount: AmountString;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
}
-export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
- buildCodecForObject<PrepareDepositRequest>()
+
+export const codecForCheckDepositRequest = (): Codec<CheckDepositRequest> =>
+ buildCodecForObject<CheckDepositRequest>()
.property("amount", codecForAmountString())
.property("depositPaytoUri", codecForString())
- .build("PrepareDepositRequest");
+ .property("clientCancellationId", codecOptional(codecForString()))
+ .build("CheckDepositRequest");
-export interface PrepareDepositResponse {
+export interface CheckDepositResponse {
totalDepositCost: AmountString;
effectiveDepositAmount: AmountString;
fees: DepositGroupFees;
@@ -2727,6 +2743,11 @@ export interface PayCoinSelection {
* How much of the deposit fees is the customer paying?
*/
customerDepositFees: AmountString;
+
+ /**
+ * How much of the deposit fees does the exchange charge in total?
+ */
+ totalDepositFees: AmountString;
}
export interface ProspectivePayCoinSelection {
@@ -2755,6 +2776,25 @@ export interface CheckPeerPushDebitRequest {
* FIXME: Allow specifying the instructed amount type.
*/
amount: AmountString;
+
+ /**
+ * Restrict the scope of funds that can be spent via the given
+ * scope info.
+ */
+ restrictScope?: ScopeInfo;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
}
export const codecForCheckPeerPushDebitRequest =
@@ -2762,6 +2802,7 @@ export const codecForCheckPeerPushDebitRequest =
buildCodecForObject<CheckPeerPushDebitRequest>()
.property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("amount", codecForAmountString())
+ .property("clientCancellationId", codecOptional(codecForString()))
.build("CheckPeerPushDebitRequest");
export interface CheckPeerPushDebitResponse {
@@ -2782,6 +2823,13 @@ export interface CheckPeerPushDebitResponse {
export interface InitiatePeerPushDebitRequest {
exchangeBaseUrl?: string;
+
+ /**
+ * Restrict the scope of funds that can be spent via the given
+ * scope info.
+ */
+ restrictScope?: ScopeInfo;
+
partialContractTerms: PeerContractTerms;
}
@@ -2894,12 +2942,27 @@ export const codecForAcceptPeerPullPaymentRequest =
export interface CheckPeerPullCreditRequest {
exchangeBaseUrl?: string;
amount: AmountString;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
}
+
export const codecForPreparePeerPullPaymentRequest =
(): Codec<CheckPeerPullCreditRequest> =>
buildCodecForObject<CheckPeerPullCreditRequest>()
.property("amount", codecForAmountString())
.property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .property("clientCancellationId", codecOptional(codecForString()))
.build("CheckPeerPullCreditRequest");
export interface CheckPeerPullCreditResponse {
@@ -2913,6 +2976,7 @@ export interface CheckPeerPullCreditResponse {
*/
numCoins: number;
}
+
export interface InitiatePeerPullCreditRequest {
exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
@@ -3060,9 +3124,14 @@ export interface WalletContractData {
minimumAge?: number;
}
+export interface TestingWaitExchangeStateRequest {
+ exchangeBaseUrl: string;
+ walletKycStatus?: ExchangeWalletKycStatus;
+}
+
export interface TestingWaitTransactionRequest {
transactionId: TransactionIdStr;
- txState: TransactionState;
+ txState: TransactionState | TransactionState[];
}
export interface TestingGetReserveHistoryRequest {
@@ -3371,3 +3440,117 @@ export const codecForHintNetworkAvailabilityRequest =
buildCodecForObject<HintNetworkAvailabilityRequest>()
.property("isNetworkAvailable", codecForBoolean())
.build("HintNetworkAvailabilityRequest");
+
+export interface GetDepositWireTypesForCurrencyRequest {
+ currency: string;
+ /**
+ * Optional scope info to further restrict the result.
+ * Currency must match the currency field.
+ */
+ scopeInfo?: ScopeInfo;
+}
+
+export const codecForGetDepositWireTypesForCurrencyRequest =
+ (): Codec<GetDepositWireTypesForCurrencyRequest> =>
+ buildCodecForObject<GetDepositWireTypesForCurrencyRequest>()
+ .property("currency", codecForString())
+ .property("scopeInfo", codecOptional(codecForScopeInfo()))
+ .build("GetDepositWireTypesForCurrencyRequest");
+
+/**
+ * Response with wire types that are supported for a deposit.
+ *
+ * In the future, we might surface more information here, such as debit restrictions
+ * by the exchange, which then can be shown by UIs to the user before they
+ * enter their payment information.
+ */
+export interface GetDepositWireTypesForCurrencyResponse {
+ /**
+ * @deprecated, use wireTypeDetails instead.
+ */
+ wireTypes: string[];
+
+ /**
+ * Details for each wire type.
+ */
+ wireTypeDetails: WireTypeDetails[];
+}
+
+export interface WireTypeDetails {
+ paymentTargetType: string;
+
+ /**
+ * Allowed hostnames for the deposit payto URI.
+ * Only applicable to x-taler-bank.
+ */
+ talerBankHostnames?: string[];
+}
+
+export interface GetQrCodesForPaytoRequest {
+ paytoUri: string;
+}
+
+export const codecForGetQrCodesForPaytoRequest = () =>
+ buildCodecForObject<GetQrCodesForPaytoRequest>()
+ .property("paytoUri", codecForString())
+ .build("GetQrCodesForPaytoRequest");
+
+export interface GetQrCodesForPaytoResponse {
+ codes: QrCodeSpec[];
+}
+
+export interface GetBankingChoicesForPaytoRequest {
+ paytoUri: string;
+}
+
+export const codecForGetBankingChoicesForPaytoRequest = () =>
+ buildCodecForObject<GetBankingChoicesForPaytoRequest>()
+ .property("paytoUri", codecForString())
+ .build("GetBankingChoicesForPaytoRequest");
+
+export interface BankingChoiceSpec {
+ label: string;
+ // FIXME: In the future, we might also have some way to return intents here?
+ type: "link";
+ uri: string;
+}
+
+export interface GetBankingChoicesForPaytoResponse {
+ choices: BankingChoiceSpec[];
+}
+
+export type EmptyObject = Record<string, never>;
+
+export const codecForEmptyObject = (): Codec<EmptyObject> =>
+ buildCodecForObject<EmptyObject>().build("EmptyObject");
+
+export interface TestingWaitWalletKycRequest {
+ exchangeBaseUrl: string;
+ amount: AmountString;
+ /**
+ * Do we wait for the KYC to be passed (true),
+ * or do we already return if legitimization is
+ * required (false).
+ */
+ passed: boolean;
+}
+
+export const codecForTestingWaitWalletKycRequest =
+ (): Codec<TestingWaitWalletKycRequest> =>
+ buildCodecForObject<TestingWaitWalletKycRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForAmountString())
+ .property("passed", codecForBoolean())
+ .build("TestingWaitWalletKycRequest");
+
+export interface StartExchangeWalletKycRequest {
+ exchangeBaseUrl: string;
+ amount: AmountString;
+}
+
+export const codecForStartExchangeWalletKycRequest =
+ (): Codec<StartExchangeWalletKycRequest> =>
+ buildCodecForObject<StartExchangeWalletKycRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForAmountString())
+ .build("StartExchangeWalletKycRequest");
diff --git a/packages/taler-util/src/types-taler-wire-gateway.ts b/packages/taler-util/src/types-taler-wire-gateway.ts
new file mode 100644
index 000000000..fd1eb7263
--- /dev/null
+++ b/packages/taler-util/src/types-taler-wire-gateway.ts
@@ -0,0 +1,278 @@
+/*
+ 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 Affero 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 Affero General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {
+ Codec,
+ TalerWireGatewayApi,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAmountString,
+ codecForConstString,
+ codecForList,
+ codecForNumber,
+ codecForString,
+} from "./index.js";
+import { PaytoString, codecForPaytoString } from "./payto.js";
+import { codecForTimestamp } from "./time.js";
+import {
+ AmountString,
+ EddsaPublicKey,
+ HashCode,
+ SafeUint64,
+ ShortHashCode,
+ Timestamp,
+ WadId,
+} from "./types-taler-common.js";
+
+export interface TransferResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+}
+
+export interface TransferRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // transaction_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: HashCode;
+
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Base URL of the exchange. Shall be included by the bank gateway
+ // in the appropriate section of the wire transfer details.
+ exchange_base_url: string;
+
+ // Wire transfer identifier chosen by the exchange,
+ // used by the merchant to identify the Taler order(s)
+ // associated with this wire transfer.
+ wtid: ShortHashCode;
+
+ // The recipient's account identifier as a payto URI.
+ credit_account: PaytoString;
+}
+
+export interface IncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: IncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if incoming transaction is empty
+ credit_account?: PaytoString;
+}
+
+// Union discriminated by the "type" field.
+export type IncomingBankTransaction =
+ | IncomingReserveTransaction
+ | IncomingWadTransaction;
+
+export interface IncomingReserveTransaction {
+ type: "RESERVE";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // The reserve public key extracted from the transaction details.
+ reserve_pub: EddsaPublicKey;
+}
+
+export interface IncomingWadTransaction {
+ type: "WAD";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ credit_account: PaytoString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // Base URL of the exchange that originated the wad.
+ origin_exchange_url: string;
+
+ // The reserve public key extracted from the transaction details.
+ wad_id: WadId;
+}
+
+export interface OutgoingHistory {
+ // Array of outgoing transactions.
+ outgoing_transactions: OutgoingBankTransaction[];
+
+ // Payto URI to identify the sender of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if outgoing transactions is empty
+ debit_account?: PaytoString;
+}
+
+export interface OutgoingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ credit_account: PaytoString;
+
+ // The wire transfer ID in the outgoing transaction.
+ wtid: ShortHashCode;
+
+ // Base URL of the exchange.
+ exchange_base_url: string;
+}
+
+export interface AddIncomingRequest {
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Reserve public key that is included in the wire transfer details
+ // to identify the reserve that is being topped up.
+ reserve_pub: EddsaPublicKey;
+
+ // Account (as payto URI) that makes the wire transfer to the exchange.
+ // Usually this account must be created by the test harness before this API is
+ // used. An exception is the "exchange-fakebank", where any debit account can be
+ // specified, as it is automatically created.
+ debit_account: PaytoString;
+}
+
+export interface AddIncomingResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+}
+
+export const codecForTransferResponse =
+ (): Codec<TalerWireGatewayApi.TransferResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.TransferResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.TransferResponse");
+
+export const codecForIncomingHistory =
+ (): Codec<TalerWireGatewayApi.IncomingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForIncomingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.IncomingHistory");
+
+export const codecForIncomingBankTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingBankTransaction> =>
+ buildCodecForUnion<TalerWireGatewayApi.IncomingBankTransaction>()
+ .discriminateOn("type")
+ .alternative("RESERVE", codecForIncomingReserveTransaction())
+ .alternative("WAD", codecForIncomingWadTransaction())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForIncomingReserveTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingReserveTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("reserve_pub", codecForString())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("RESERVE"))
+ .build("TalerWireGatewayApi.IncomingReserveTransaction");
+
+export const codecForIncomingWadTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("origin_exchange_url", codecForString())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("WAD"))
+ .property("wad_id", codecForString())
+ .build("TalerWireGatewayApi.IncomingWadTransaction");
+
+export const codecForOutgoingHistory =
+ (): Codec<TalerWireGatewayApi.OutgoingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
+ .property("debit_account", codecForPaytoString())
+ .property(
+ "outgoing_transactions",
+ codecForList(codecForOutgoingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.OutgoingHistory");
+
+export const codecForOutgoingBankTransaction =
+ (): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("exchange_base_url", codecForString())
+ .property("row_id", codecForNumber())
+ .property("wtid", codecForString())
+ .build("TalerWireGatewayApi.OutgoingBankTransaction");
+
+export const codecForAddIncomingResponse =
+ (): Codec<TalerWireGatewayApi.AddIncomingResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.AddIncomingResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.AddIncomingResponse");
diff --git a/packages/taler-util/src/types-test.ts b/packages/taler-util/src/types.test.ts
index 6acd2c26e..bea352128 100644
--- a/packages/taler-util/src/types-test.ts
+++ b/packages/taler-util/src/types.test.ts
@@ -15,15 +15,14 @@
*/
import test from "ava";
-import { codecForMerchantContractTerms as codecForContractTerms } from "./taler-types.js";
+import { codecForContractTerms, MerchantContractTerms } from "./index.js";
test("contract terms validation", (t) => {
const c = {
nonce: "123123123",
h_wire: "123",
amount: "EUR:1.5",
- auditors: [],
- exchanges: [{ master_pub: "foo", url: "foo" }],
+ exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }],
fulfillment_url: "foo",
max_fee: "EUR:1.5",
merchant_pub: "12345",
@@ -37,7 +36,7 @@ test("contract terms validation", (t) => {
summary: "hello",
timestamp: { t_s: 42 },
wire_method: "test",
- };
+ } satisfies MerchantContractTerms;
codecForContractTerms().decode(c);
@@ -59,8 +58,7 @@ test("contract terms validation (locations)", (t) => {
nonce: "123123123",
h_wire: "123",
amount: "EUR:1.5",
- auditors: [],
- exchanges: [{ master_pub: "foo", url: "foo" }],
+ exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }],
fulfillment_url: "foo",
max_fee: "EUR:1.5",
merchant_pub: "12345",
@@ -83,7 +81,7 @@ test("contract terms validation (locations)", (t) => {
country: "FR",
town: "Rennes",
},
- };
+ } satisfies MerchantContractTerms;
const r = codecForContractTerms().decode(c);
diff --git a/packages/taler-util/src/url.ts b/packages/taler-util/src/url.ts
index 149997f3f..1b5626626 100644
--- a/packages/taler-util/src/url.ts
+++ b/packages/taler-util/src/url.ts
@@ -94,7 +94,7 @@ if (useOwnUrlImp || !_URL) {
_URL = URLImpl;
}
-export const URL: URLCtor = _URL;
+export const URL = _URL;
// @ts-ignore
let _URLSearchParams = globalThis.URLSearchParams;
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
index 13abf5397..bc2c682fc 100644
--- a/packages/taler-util/src/whatwg-url.ts
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -424,6 +424,10 @@ export class URLSearchParamsImpl {
return output;
}
+ entries() {
+ return [...this._list.map((x) => [x[0], x[1]])];
+ }
+
forEach(
callbackfn: (
value: string,
@@ -1907,6 +1911,7 @@ function parseURL(
});
}
+const NativeURL = typeof URL !== "undefined" ? URL : undefined;
export class URLImpl {
//Include URL type for "url" and "base" params.
constructor(url: string | URL, base?: string | URL) {
@@ -2120,6 +2125,21 @@ export class URLImpl {
return this.href;
}
+ static createObjectURL(blob: Blob) {
+ if (!NativeURL)
+ throw new Error(
+ "This method requires a native implementation, which does not exist",
+ );
+ return NativeURL.createObjectURL(blob);
+ }
+ static revokeObjectURL(url: string) {
+ if (!NativeURL)
+ throw new Error(
+ "This method requires a native implementation, which does not exist",
+ );
+ return NativeURL.revokeObjectURL(url);
+ }
+
// FIXME: type!
_url: any;
_query: any;
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index 5fa99e801..c924e5ec4 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,129 @@
+taler-wallet-cli (0.13.4) unstable; urgency=low
+
+ * Release 0.13.4
+
+ -- Florian Dold <dold@taler.net> Thu, 19 Sep 2024 14:02:11 +0200
+
+taler-wallet-cli (0.13.3) unstable; urgency=low
+
+ * Release 0.13.3
+
+ -- Florian Dold <dold@taler.net> Tue, 17 Sep 2024 19:03:34 +0200
+
+taler-wallet-cli (0.13.2) unstable; urgency=low
+
+ * Release 0.13.2
+
+ -- Florian Dold <dold@taler.net> Wed, 11 Sep 2024 15:56:08 +0200
+
+taler-wallet-cli (0.13.1) unstable; urgency=low
+
+ * Release 0.13.1
+
+ -- Florian Dold <dold@taler.net> Wed, 28 Aug 2024 23:42:37 +0200
+
+taler-wallet-cli (0.13.0) unstable; urgency=low
+
+ * Release 0.13.0
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 20:55:34 +0200
+
+taler-wallet-cli (0.12.14~dev.1) unstable; urgency=low
+
+ * Release 0.12.14-dev.1
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 00:15:09 +0200
+
+taler-wallet-cli (0.12.14-dev.1) unstable; urgency=low
+
+ * Release 0.12.14-dev.1
+
+ -- Florian Dold <dold@taler.net> Tue, 27 Aug 2024 00:11:56 +0200
+
+taler-wallet-cli (0.12.13) unstable; urgency=low
+
+ * Release 0.12.13
+
+ -- Florian Dold <dold@taler.net> Sun, 25 Aug 2024 14:21:08 +0200
+
+taler-wallet-cli (0.12.12) unstable; urgency=low
+
+ * Release 0.12.12
+
+ -- Florian Dold <dold@taler.net> Mon, 12 Aug 2024 22:45:00 -0300
+
+taler-wallet-cli (0.12.11) unstable; urgency=low
+
+ * Release 0.12.11
+
+ -- Florian Dold <dold@taler.net> Sun, 11 Aug 2024 22:31:39 +0200
+
+taler-wallet-cli (0.12.10) unstable; urgency=low
+
+ * Release 0.12.10
+
+ -- Florian Dold <dold@taler.net> Sun, 11 Aug 2024 17:17:11 +0200
+
+taler-wallet-cli (0.12.9) unstable; urgency=low
+
+ * Release 0.12.9
+
+ -- Florian Dold <dold@taler.net> Thu, 08 Aug 2024 16:51:49 +0200
+
+taler-wallet-cli (0.12.8) unstable; urgency=low
+
+ * Release 0.12.8
+
+ -- Florian Dold <dold@taler.net> Fri, 02 Aug 2024 11:37:02 -0600
+
+taler-wallet-cli (0.12.7) unstable; urgency=low
+
+ * Release 0.12.7
+
+ -- Florian Dold <dold@taler.net> Mon, 29 Jul 2024 10:57:50 +0200
+
+taler-wallet-cli (0.12.6) unstable; urgency=low
+
+ * Release 0.12.6
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Jul 2024 12:36:42 -0600
+
+taler-wallet-cli (0.12.5) unstable; urgency=low
+
+ * Release 0.12.5
+
+ -- Florian Dold <dold@taler.net> Wed, 17 Jul 2024 09:34:49 -0600
+
+taler-wallet-cli (0.12.4) unstable; urgency=low
+
+ * Release 0.12.4
+
+ -- Florian Dold <dold@taler.net> Mon, 15 Jul 2024 08:39:36 -0600
+
+taler-wallet-cli (0.12.3) unstable; urgency=low
+
+ * Release 0.12.3
+
+ -- Florian Dold <dold@taler.net> Mon, 15 Jul 2024 08:36:26 -0600
+
+taler-wallet-cli (0.12.2) unstable; urgency=low
+
+ * Release 0.12.2
+
+ -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200
+
+taler-wallet-cli (0.12.1) unstable; urgency=low
+
+ * Release 0.12.1
+
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600
+
+taler-wallet-cli (0.12.0) unstable; urgency=low
+
+ * Release 0.12.0
+
+ -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 16:17:52 +0200
+
taler-wallet-cli (0.11.4) unstable; urgency=low
* Release 0.11.4
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index ecc8252e6..973fe126b 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 5bde7db01..dc11f83b3 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -21,6 +21,7 @@ import {
AbsoluteTime,
addPaytoQueryParams,
AgeRestriction,
+ Amounts,
AmountString,
codecForList,
codecForString,
@@ -334,12 +335,26 @@ async function withWallet<T>(
writeObservabilityLog(notif);
};
+ let walletSocketPath: string | undefined = undefined;
+
+ const connEnvName = "TALER_WALLET_CONNECTION";
+
if (walletCliArgs.wallet.walletConnection) {
+ walletSocketPath = walletCliArgs.wallet.walletConnection;
+ logger.info(`using wallet socket from command line (${walletSocketPath})`);
+ } else if (!!process.env[connEnvName]) {
+ walletSocketPath = process.env[connEnvName];
+ logger.info(
+ `using wallet socket from ${connEnvName} (${walletSocketPath})`,
+ );
+ }
+
+ if (walletSocketPath) {
logger.info("creating remote wallet");
const w = await createRemoteWallet({
name: "wallet",
notificationHandler: onNotif,
- socketFilename: walletCliArgs.wallet.walletConnection,
+ socketFilename: walletSocketPath,
});
const ctx: WalletContext = {
makeCoreApiRequest(operation, payload) {
@@ -722,6 +737,16 @@ walletCli
},
);
console.log("withdrawInfo", withdrawInfo);
+ let amount: AmountString | undefined = undefined;
+ if (withdrawInfo.editableAmount) {
+ if (withdrawInfo.amount) {
+ console.log(`Default amount: ${withdrawInfo.amount}`);
+ }
+ const res = await readlinePrompt(
+ `Amount (in ${withdrawInfo.currency}): `,
+ );
+ amount = Amounts.stringify(Amounts.parseOrThrow(res));
+ }
const selectedExchange =
args.handleUri.withdrawalExchange ??
withdrawInfo.defaultExchangeBaseUrl;
@@ -732,6 +757,10 @@ walletCli
processExit(1);
return;
}
+ // FIXME: Maybe prompt for this?
+ await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, {
+ exchangeBaseUrl: selectedExchange,
+ });
const res = await wallet.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
@@ -1365,13 +1394,8 @@ advancedCli
advancedCli
.subcommand("pending", "pending", { help: "Show pending operations." })
.action(async (args) => {
- await withWallet(args, { lazyTaskLoop: true }, async (wallet) => {
- const pending = await wallet.client.call(
- WalletApiOperation.GetPendingOperations,
- {},
- );
- console.log(JSON.stringify(pending, undefined, 2));
- });
+ console.error("Subcommand removed due to deprecation.");
+ process.exit(1);
});
advancedCli
@@ -1695,10 +1719,25 @@ advancedCli
await withWallet(args, { lazyTaskLoop: true }, async (wallet) => {
const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {});
for (const coin of coins.coins) {
- console.log(`coin ${coin.coin_pub}`);
- console.log(` exchange ${coin.exchange_base_url}`);
- console.log(` denomPubHash ${coin.denom_pub_hash}`);
- console.log(` status ${coin.coin_status}`);
+ console.log(`coin ${coin.coinPub}`);
+ console.log(` exchange ${coin.exchangeBaseUrl}`);
+ console.log(` denomPubHash ${coin.denomPubHash}`);
+ console.log(` status ${coin.coinStatus}`);
+ if (coin.history.length > 0) {
+ console.log(` history`);
+ for (const hi of coin.history) {
+ switch (hi.type) {
+ case "spend":
+ console.log(` spend ${hi.transactionId} ${hi.amount}`);
+ break;
+ case "refresh":
+ console.log(` refresh ${hi.transactionId} ${hi.amount}`);
+ break;
+ default:
+ console.log(` unknown (${hi.type})`);
+ }
+ }
+ }
}
});
});
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index c710861d3..d030ff63c 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index 09d5ae75d..c5febd278 100644
--- a/packages/taler-wallet-core/src/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -427,6 +427,10 @@ export async function processBackupForProvider(
wex: WalletExecutionContext,
backupProviderBaseUrl: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
const provider = await wex.db.runReadOnlyTx(
{ storeNames: ["backupProviders"] },
async (tx) => {
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 381028906..20eb71a7f 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -94,7 +94,6 @@ interface WalletBalance {
pendingIncoming: AmountJson;
pendingOutgoing: AmountJson;
flagIncomingKyc: boolean;
- flagIncomingAml: boolean;
flagIncomingConfirmation: boolean;
flagOutgoingKyc: boolean;
}
@@ -170,7 +169,6 @@ class BalancesStore {
available: zero,
pendingIncoming: zero,
pendingOutgoing: zero,
- flagIncomingAml: false,
flagIncomingConfirmation: false,
flagIncomingKyc: false,
flagOutgoingKyc: false,
@@ -210,14 +208,6 @@ class BalancesStore {
b.pendingOutgoing = Amounts.add(b.pendingOutgoing, amount).amount;
}
- async setFlagIncomingAml(
- currency: string,
- exchangeBaseUrl: string,
- ): Promise<void> {
- const b = await this.initBalance(currency, exchangeBaseUrl);
- b.flagIncomingAml = true;
- }
-
async setFlagIncomingKyc(
currency: string,
exchangeBaseUrl: string,
@@ -254,9 +244,6 @@ class BalancesStore {
.forEach((c) => {
const v = balanceStore[c];
const flags: BalanceFlag[] = [];
- if (v.flagIncomingAml) {
- flags.push(BalanceFlag.IncomingAml);
- }
if (v.flagIncomingKyc) {
flags.push(BalanceFlag.IncomingKyc);
}
@@ -373,7 +360,11 @@ export async function getBalancesInsideTransaction(
case WithdrawalGroupStatus.SuspendedQueryingStatus:
// Pending, but no special flag.
break;
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
case WithdrawalGroupStatus.PendingKyc: {
checkDbInvariant(
wg.denomsSel !== undefined,
@@ -387,20 +378,6 @@ export async function getBalancesInsideTransaction(
await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl);
break;
}
- case WithdrawalGroupStatus.PendingAml:
- case WithdrawalGroupStatus.SuspendedAml: {
- checkDbInvariant(
- wg.denomsSel !== undefined,
- "wg in aml state should have been initialized",
- );
- checkDbInvariant(
- wg.exchangeBaseUrl !== undefined,
- "wg in kyc state should have been initialized",
- );
- const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
- await balanceStore.setFlagIncomingAml(currency, wg.exchangeBaseUrl);
- break;
- }
case WithdrawalGroupStatus.PendingRegisteringBank: {
if (wg.denomsSel && wg.exchangeBaseUrl) {
const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue);
@@ -472,14 +449,13 @@ export async function getBalancesInsideTransaction(
for (const [e, x] of Object.entries(perExchange)) {
const currency = Amounts.currencyOf(dgRecord.amount);
switch (dgRecord.operationStatus) {
- case DepositOperationStatus.SuspendedKyc:
- case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
+ case DepositOperationStatus.PendingAggregateKyc:
await balanceStore.setFlagOutgoingKyc(currency, e);
}
-
switch (dgRecord.operationStatus) {
- case DepositOperationStatus.SuspendedKyc:
- case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
+ case DepositOperationStatus.PendingAggregateKyc:
case DepositOperationStatus.PendingTrack:
case DepositOperationStatus.SuspendedAborting:
case DepositOperationStatus.SuspendedDeposit:
@@ -575,12 +551,12 @@ export interface PaymentBalanceDetails {
balanceAgeAcceptable: AmountJson;
/**
- * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ * Balance of type "receiver-acceptable" (see balance.ts for definition).
*/
balanceReceiverAcceptable: AmountJson;
/**
- * Balance of type "merchant-depositable" (see balance.ts for definition).
+ * Balance of type "receiver-depositable" (see balance.ts for definition).
*/
balanceReceiverDepositable: AmountJson;
@@ -591,7 +567,11 @@ export interface PaymentBalanceDetails {
*/
balanceExchangeDepositable: AmountJson;
- maxEffectiveSpendAmount: AmountJson;
+ /**
+ * Estimated maximum amount that the wallet could pay for, under the assumption
+ * that the merchant pays absolutely no fees.
+ */
+ maxMerchantEffectiveDepositAmount: AmountJson;
}
export async function getPaymentBalanceDetails(
@@ -633,7 +613,7 @@ export async function getPaymentBalanceDetailsInTx(
balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
balanceReceiverAcceptable: Amounts.zeroOfCurrency(req.currency),
balanceReceiverDepositable: Amounts.zeroOfCurrency(req.currency),
- maxEffectiveSpendAmount: Amounts.zeroOfCurrency(req.currency),
+ maxMerchantEffectiveDepositAmount: Amounts.zeroOfCurrency(req.currency),
balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency),
};
@@ -742,13 +722,13 @@ export async function getPaymentBalanceDetailsInTx(
merchantExchangeAcceptable &&
merchantExchangeDepositable
) {
- d.maxEffectiveSpendAmount = Amounts.add(
- d.maxEffectiveSpendAmount,
+ d.maxMerchantEffectiveDepositAmount = Amounts.add(
+ d.maxMerchantEffectiveDepositAmount,
Amounts.mult(ca.value, ca.freshCoinCount).amount,
).amount;
- d.maxEffectiveSpendAmount = Amounts.sub(
- d.maxEffectiveSpendAmount,
+ d.maxMerchantEffectiveDepositAmount = Amounts.sub(
+ d.maxMerchantEffectiveDepositAmount,
Amounts.mult(denom.feeDeposit, ca.freshCoinCount).amount,
).amount;
}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
index c7cb2857e..4984552f8 100644
--- a/packages/taler-wallet-core/src/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -15,17 +15,21 @@
*/
import {
AbsoluteTime,
+ AmountJson,
AmountString,
Amounts,
DenomKeyType,
+ DenominationPubKey,
Duration,
+ TalerProtocolTimestamp,
j2s,
} from "@gnu-taler/taler-util";
import test from "ava";
import {
- AvailableDenom,
+ AvailableCoinsOfDenom,
CoinSelectionTally,
emptyTallyForPeerPayment,
+ testing_getMaxDepositAmountForAvailableCoins,
testing_selectGreedy,
} from "./coinSelection.js";
@@ -42,7 +46,9 @@ const inThePast = AbsoluteTime.toProtocolTimestamp(
test("p2p: should select the coin", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:2");
- const tally = emptyTallyForPeerPayment(instructedAmount);
+ const tally = emptyTallyForPeerPayment({
+ instructedAmount,
+ });
t.log(`tally before: ${j2s(tally)}`);
const coins = testing_selectGreedy(
{
@@ -76,7 +82,9 @@ test("p2p: should select the coin", (t) => {
test("p2p: should select 3 coins", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:20");
- const tally = emptyTallyForPeerPayment(instructedAmount);
+ const tally = emptyTallyForPeerPayment({
+ instructedAmount,
+ });
const coins = testing_selectGreedy(
{
wireFeesPerExchange: {},
@@ -108,7 +116,9 @@ test("p2p: should select 3 coins", (t) => {
test("p2p: can't select since the instructed amount is too high", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:60");
- const tally = emptyTallyForPeerPayment(instructedAmount);
+ const tally = emptyTallyForPeerPayment({
+ instructedAmount,
+ });
const coins = testing_selectGreedy(
{
wireFeesPerExchange: {},
@@ -138,6 +148,7 @@ test("pay: select one coin to pay with fee", (t) => {
customerWireFees: zero,
wireFeeCoveredForExchange: new Set<string>(),
lastDepositFee: zero,
+ totalDepositFees: zero,
} satisfies CoinSelectionTally;
const coins = testing_selectGreedy(
{
@@ -180,7 +191,7 @@ function createCandidates(
numAvailable: number;
fromExchange: string;
}[],
-): AvailableDenom[] {
+): AvailableCoinsOfDenom[] {
return ar.map((r, idx) => {
return {
denomPub: {
@@ -269,7 +280,9 @@ test("p2p: regression STATER", (t) => {
},
];
const instructedAmount = Amounts.parseOrThrow("STATER:1");
- const tally = emptyTallyForPeerPayment(instructedAmount);
+ const tally = emptyTallyForPeerPayment({
+ instructedAmount,
+ });
const res = testing_selectGreedy(
{
wireFeesPerExchange: {},
@@ -279,3 +292,163 @@ test("p2p: regression STATER", (t) => {
);
t.assert(!!res);
});
+
+function makeCurrencyHelper(currency: string) {
+ return (sx: TemplateStringsArray, ...vx: any[]) => {
+ const s = String.raw({ raw: sx }, ...vx);
+ return Amounts.parseOrThrow(`${currency}:${s}`);
+ };
+}
+
+type TestCoin = [AmountJson, number];
+
+const kudos = makeCurrencyHelper("kudos");
+
+function defaultFeeConfig(
+ value: AmountJson,
+ totalAvailable: number,
+): AvailableCoinsOfDenom {
+ return {
+ denomPub: undefined as any as DenominationPubKey,
+ denomPubHash: "123",
+ feeDeposit: `KUDOS:0.01`,
+ feeRefresh: `KUDOS:0.01`,
+ feeRefund: `KUDOS:0.01`,
+ feeWithdraw: `KUDOS:0.01`,
+ exchangeBaseUrl: "2",
+ maxAge: 0,
+ numAvailable: totalAvailable,
+ stampExpireDeposit: TalerProtocolTimestamp.never(),
+ stampExpireLegal: TalerProtocolTimestamp.never(),
+ stampExpireWithdraw: TalerProtocolTimestamp.never(),
+ stampStart: TalerProtocolTimestamp.never(),
+ value: Amounts.stringify(value),
+ };
+}
+
+test("deposit max 35", (t) => {
+ const coinList: TestCoin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = testing_getMaxDepositAmountForAvailableCoins(
+ {
+ currency: "KUDOS",
+ },
+ {
+ coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ currentWireFeePerExchange: {
+ "2": kudos`0`,
+ },
+ },
+ );
+ t.is(Amounts.stringifyValue(result.rawAmount), "34.9");
+ t.is(Amounts.stringifyValue(result.effectiveAmount), "35");
+});
+
+test("deposit max 35 with wirefee", (t) => {
+ const coinList: TestCoin[] = [
+ [kudos`2`, 5],
+ [kudos`5`, 5],
+ ];
+ const result = testing_getMaxDepositAmountForAvailableCoins(
+ {
+ currency: "KUDOS",
+ },
+ {
+ coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ currentWireFeePerExchange: {
+ "2": kudos`1`,
+ },
+ },
+ );
+ t.is(Amounts.stringifyValue(result.rawAmount), "33.9");
+ t.is(Amounts.stringifyValue(result.effectiveAmount), "35");
+});
+
+test("deposit max repeated denom", (t) => {
+ const coinList: TestCoin[] = [
+ [kudos`2`, 1],
+ [kudos`2`, 1],
+ [kudos`5`, 1],
+ ];
+ const result = testing_getMaxDepositAmountForAvailableCoins(
+ {
+ currency: "KUDOS",
+ },
+ {
+ coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ currentWireFeePerExchange: {
+ "2": kudos`0`,
+ },
+ },
+ );
+ t.is(Amounts.stringifyValue(result.rawAmount), "8.97");
+ t.is(Amounts.stringifyValue(result.effectiveAmount), "9");
+});
+
+test("demo: deposit max after withdraw raw 25", (t) => {
+ const coinList: TestCoin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 2],
+ [kudos`5`, 0],
+ [kudos`10`, 2],
+ ];
+ const result = testing_getMaxDepositAmountForAvailableCoins(
+ {
+ currency: "KUDOS",
+ },
+ {
+ coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ currentWireFeePerExchange: {
+ "2": kudos`0.01`,
+ },
+ },
+ );
+ t.is(Amounts.stringifyValue(result.effectiveAmount), "24.8");
+ t.is(Amounts.stringifyValue(result.rawAmount), "24.67");
+
+ // 8 x 0.1
+ // 2 x 0.2
+ // 2 x 10.0
+ // total effective 24.8
+ // deposit fee 12 x 0.01 = 0.12
+ // wire fee 0.01
+ // total raw: 24.8 - 0.13 = 24.67
+
+ // current wallet impl fee 0.14
+});
+
+test("demo: deposit max after withdraw raw 13", (t) => {
+ const coinList: TestCoin[] = [
+ [kudos`0.1`, 8],
+ [kudos`1`, 0],
+ [kudos`2`, 1],
+ [kudos`5`, 0],
+ [kudos`10`, 1],
+ ];
+ const result = testing_getMaxDepositAmountForAvailableCoins(
+ {
+ currency: "KUDOS",
+ },
+ {
+ coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+ currentWireFeePerExchange: {
+ "2": kudos`0.01`,
+ },
+ },
+ );
+ t.is(Amounts.stringifyValue(result.effectiveAmount), "12.8");
+ t.is(Amounts.stringifyValue(result.rawAmount), "12.69");
+
+ // 8 x 0.1
+ // 1 x 0.2
+ // 1 x 10.0
+ // total effective 12.8
+ // deposit fee 10 x 0.01 = 0.10
+ // wire fee 0.01
+ // total raw: 12.8 - 0.11 = 12.69
+
+ // current wallet impl fee 0.14
+});
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index db6384c93..bc9d51ec7 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -26,25 +26,30 @@
import { GlobalIDB } from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
- AccountRestriction,
AgeRestriction,
AllowedAuditorInfo,
AllowedExchangeInfo,
AmountJson,
Amounts,
+ checkAccountRestriction,
checkDbInvariant,
checkLogicInvariant,
CoinStatus,
DenominationInfo,
ExchangeGlobalFees,
ForcedCoinSel,
- InternationalizedString,
+ GetMaxDepositAmountRequest,
+ GetMaxDepositAmountResponse,
+ GetMaxPeerPushDebitAmountRequest,
+ GetMaxPeerPushDebitAmountResponse,
j2s,
Logger,
parsePaytoUri,
PayCoinSelection,
PaymentInsufficientBalanceDetails,
ProspectivePayCoinSelection,
+ ScopeInfo,
+ ScopeType,
SelectedCoin,
SelectedProspectiveCoin,
strcmp,
@@ -54,6 +59,7 @@ import { getPaymentBalanceDetailsInTx } from "./balance.js";
import { getAutoRefreshExecuteThreshold } from "./common.js";
import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js";
import {
+ checkExchangeInScope,
ExchangeWireDetails,
getExchangeWireDetailsInTx,
} from "./exchanges.js";
@@ -86,6 +92,8 @@ export interface CoinSelectionTally {
customerDepositFees: AmountJson;
+ totalDepositFees: AmountJson;
+
customerWireFees: AmountJson;
wireFeeCoveredForExchange: Set<string>;
@@ -152,6 +160,10 @@ function tallyFees(
dfRemaining,
).amount;
tally.lastDepositFee = feeDeposit;
+ tally.totalDepositFees = Amounts.add(
+ tally.totalDepositFees,
+ feeDeposit,
+ ).amount;
}
export type SelectPayCoinsResult =
@@ -180,19 +192,32 @@ async function internalSelectPayCoins(
| { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally }
| undefined
> {
+ let restrictWireMethod;
+ if (req.depositPaytoUri) {
+ const parsedPayto = parsePaytoUri(req.depositPaytoUri);
+ if (!parsedPayto) {
+ throw Error("invalid payto URI");
+ }
+ restrictWireMethod = parsedPayto.targetType;
+ if (restrictWireMethod !== req.restrictWireMethod) {
+ logger.warn(`conflicting payto URI and wire method restriction`);
+ }
+ } else {
+ restrictWireMethod = req.restrictWireMethod;
+ }
+
const { contractTermsAmount, depositFeeLimit } = req;
- const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
- wex,
- tx,
- {
- restrictExchanges: req.restrictExchanges,
- instructedAmount: req.contractTermsAmount,
- restrictWireMethod: req.restrictWireMethod,
- depositPaytoUri: req.depositPaytoUri,
- requiredMinimumAge: req.requiredMinimumAge,
- includePendingCoins,
- },
- );
+ const candidateRes = await selectPayCandidates(wex, tx, {
+ currency: Amounts.currencyOf(req.contractTermsAmount),
+ restrictExchanges: req.restrictExchanges,
+ restrictWireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ requiredMinimumAge: req.requiredMinimumAge,
+ includePendingCoins,
+ });
+
+ const wireFeesPerExchange = candidateRes.currentWireFeePerExchange;
+ const candidateDenoms = candidateRes.coinAvailability;
if (logger.shouldLogTrace()) {
logger.trace(
@@ -210,6 +235,7 @@ async function internalSelectPayCoins(
amountDepositFeeLimitRemaining: depositFeeLimit,
customerDepositFees: Amounts.zeroOfCurrency(currency),
customerWireFees: Amounts.zeroOfCurrency(currency),
+ totalDepositFees: Amounts.zeroOfCurrency(currency),
wireFeeCoveredForExchange: new Set(),
lastDepositFee: Amounts.zeroOfCurrency(currency),
};
@@ -252,6 +278,88 @@ async function internalSelectPayCoins(
};
}
+export async function selectPayCoinsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ]
+ >,
+ req: SelectPayCoinRequestNg,
+): Promise<SelectPayCoinsResult> {
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selecting coins for ${j2s(req)}`);
+ }
+
+ const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
+
+ if (!materialAvSel) {
+ const prospectiveAvSel = await internalSelectPayCoins(wex, tx, req, true);
+
+ if (prospectiveAvSel) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvSel.sel)) {
+ const mySel = prospectiveAvSel.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ customerDepositFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerDepositFees,
+ ),
+ customerWireFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerWireFees,
+ ),
+ },
+ } satisfies SelectPayCoinsResult;
+ }
+
+ return {
+ type: "failure",
+ insufficientBalanceDetails: await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ requiredMinimumAge: req.requiredMinimumAge,
+ wireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ },
+ ),
+ } satisfies SelectPayCoinsResult;
+ }
+
+ const coinSel = await assembleSelectPayCoinsSuccessResult(
+ tx,
+ materialAvSel.sel,
+ materialAvSel.coinRes,
+ materialAvSel.tally,
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`coin selection: ${j2s(coinSel)}`);
+ }
+
+ return {
+ type: "success",
+ coinSel,
+ };
+}
+
/**
* Select coins to spend under the merchant's constraints.
*
@@ -263,10 +371,6 @@ export async function selectPayCoins(
wex: WalletExecutionContext,
req: SelectPayCoinRequestNg,
): Promise<SelectPayCoinsResult> {
- if (logger.shouldLogTrace()) {
- logger.trace(`selecting coins for ${j2s(req)}`);
- }
-
return await wex.db.runReadOnlyTx(
{
storeNames: [
@@ -279,73 +383,7 @@ export async function selectPayCoins(
],
},
async (tx) => {
- const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
-
- if (!materialAvSel) {
- const prospectiveAvSel = await internalSelectPayCoins(
- wex,
- tx,
- req,
- true,
- );
-
- if (prospectiveAvSel) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvSel.sel)) {
- const mySel = prospectiveAvSel.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- customerDepositFees: Amounts.stringify(
- prospectiveAvSel.tally.customerDepositFees,
- ),
- customerWireFees: Amounts.stringify(
- prospectiveAvSel.tally.customerWireFees,
- ),
- },
- } satisfies SelectPayCoinsResult;
- }
-
- return {
- type: "failure",
- insufficientBalanceDetails: await reportInsufficientBalanceDetails(
- wex,
- tx,
- {
- restrictExchanges: req.restrictExchanges,
- instructedAmount: req.contractTermsAmount,
- requiredMinimumAge: req.requiredMinimumAge,
- wireMethod: req.restrictWireMethod,
- depositPaytoUri: req.depositPaytoUri,
- },
- ),
- } satisfies SelectPayCoinsResult;
- }
-
- const coinSel = await assembleSelectPayCoinsSuccessResult(
- tx,
- materialAvSel.sel,
- materialAvSel.coinRes,
- materialAvSel.tally,
- );
-
- if (logger.shouldLogTrace()) {
- logger.trace(`coin selection: ${j2s(coinSel)}`);
- }
-
- return {
- type: "success",
- coinSel,
- };
+ return selectPayCoinsInTx(wex, tx, req);
},
);
}
@@ -440,6 +478,7 @@ async function assembleSelectPayCoinsSuccessResult(
coins: coinRes,
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
customerWireFees: Amounts.stringify(tally.customerWireFees),
+ totalDepositFees: Amounts.stringify(tally.totalDepositFees),
};
}
@@ -481,12 +520,16 @@ export async function reportInsufficientBalanceDetails(
let missingGlobalFees = false;
const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl);
if (!exchWire) {
+ // No wire details about the exchange known, skip!
+ continue;
+ }
+ const globalFees = getGlobalFees(exchWire);
+ if (!globalFees) {
missingGlobalFees = true;
- } else {
- const globalFees = getGlobalFees(exchWire);
- if (!globalFees) {
- missingGlobalFees = true;
- }
+ }
+ if (exchWire.currency !== Amounts.currencyOf(req.instructedAmount)) {
+ // Do not report anything for an exchange with a different currency.
+ continue;
}
const exchDet = await getPaymentBalanceDetailsInTx(wex, tx, {
restrictExchanges: {
@@ -498,7 +541,7 @@ export async function reportInsufficientBalanceDetails(
],
auditors: [],
},
- restrictWireMethods: req.wireMethod ? [req.wireMethod] : [],
+ restrictWireMethods: req.wireMethod ? [req.wireMethod] : undefined,
currency: Amounts.currencyOf(req.instructedAmount),
minAge: req.requiredMinimumAge ?? 0,
depositPaytoUri: req.depositPaytoUri,
@@ -517,7 +560,7 @@ export async function reportInsufficientBalanceDetails(
exchDet.balanceReceiverDepositable,
),
maxEffectiveSpendAmount: Amounts.stringify(
- exchDet.maxEffectiveSpendAmount,
+ exchDet.maxMerchantEffectiveDepositAmount,
),
missingGlobalFees,
};
@@ -537,7 +580,9 @@ export async function reportInsufficientBalanceDetails(
balanceReceiverDepositable: Amounts.stringify(
details.balanceReceiverDepositable,
),
- maxEffectiveSpendAmount: Amounts.stringify(details.maxEffectiveSpendAmount),
+ maxEffectiveSpendAmount: Amounts.stringify(
+ details.maxMerchantEffectiveDepositAmount,
+ ),
perExchange,
};
}
@@ -578,7 +623,7 @@ export interface SelectGreedyRequest {
function selectGreedy(
req: SelectGreedyRequest,
- candidateDenoms: AvailableDenom[],
+ candidateDenoms: AvailableCoinsOfDenom[],
tally: CoinSelectionTally,
): SelResult | undefined {
const selectedDenom: SelResult = {};
@@ -641,7 +686,7 @@ function selectGreedy(
function selectForced(
req: SelectPayCoinRequestNg,
- candidateDenoms: AvailableDenom[],
+ candidateDenoms: AvailableCoinsOfDenom[],
): SelResult | undefined {
const selectedDenom: SelResult = {};
@@ -683,31 +728,6 @@ function selectForced(
return selectedDenom;
}
-export function checkAccountRestriction(
- paytoUri: string,
- restrictions: AccountRestriction[],
-): { ok: boolean; hint?: string; hintI18n?: InternationalizedString } {
- for (const myRestriction of restrictions) {
- switch (myRestriction.type) {
- case "deny":
- return { ok: false };
- case "regex": {
- const regex = new RegExp(myRestriction.payto_regex);
- if (!regex.test(paytoUri)) {
- return {
- ok: false,
- hint: myRestriction.human_hint,
- hintI18n: myRestriction.human_hint_i18n,
- };
- }
- }
- }
- }
- return {
- ok: true,
- };
-}
-
export interface SelectPayCoinRequestNg {
restrictExchanges: ExchangeRestrictionSpec | undefined;
restrictWireMethod: string;
@@ -727,7 +747,7 @@ export interface SelectPayCoinRequestNg {
depositPaytoUri?: string;
}
-export type AvailableDenom = DenominationInfo & {
+export type AvailableCoinsOfDenom = DenominationInfo & {
maxAge: number;
numAvailable: number;
};
@@ -808,7 +828,7 @@ function checkExchangeAccepted(
}
interface SelectPayCandidatesRequest {
- instructedAmount: AmountJson;
+ currency: string;
restrictWireMethod: string | undefined;
depositPaytoUri?: string;
restrictExchanges: ExchangeRestrictionSpec | undefined;
@@ -822,18 +842,23 @@ interface SelectPayCandidatesRequest {
includePendingCoins: boolean;
}
+export interface PayCoinCandidates {
+ coinAvailability: AvailableCoinsOfDenom[];
+ currentWireFeePerExchange: Record<string, AmountJson>;
+}
+
async function selectPayCandidates(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<
["exchanges", "coinAvailability", "exchangeDetails", "denominations"]
>,
req: SelectPayCandidatesRequest,
-): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
+): Promise<PayCoinCandidates> {
// FIXME: Use the existing helper (from balance.ts) to
// get acceptable exchanges.
logger.shouldLogTrace() &&
logger.trace(`selecting available coin candidates for ${j2s(req)}`);
- const denoms: AvailableDenom[] = [];
+ const denoms: AvailableCoinsOfDenom[] = [];
const exchanges = await tx.exchanges.iter().toArray();
const wfPerExchange: Record<string, AmountJson> = {};
for (const exchange of exchanges) {
@@ -842,7 +867,7 @@ async function selectPayCandidates(
exchange.baseUrl,
);
// 1. exchange has same currency
- if (exchangeDetails?.currency !== req.instructedAmount.currency) {
+ if (exchangeDetails?.currency !== req.currency) {
logger.shouldLogTrace() &&
logger.trace(`skipping ${exchange.baseUrl} due to currency mismatch`);
continue;
@@ -910,7 +935,10 @@ async function selectPayCandidates(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`,
+ );
if (denom.isRevoked) {
logger.trace("denom is revoked");
continue;
@@ -946,7 +974,10 @@ async function selectPayCandidates(
Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
strcmp(o1.denomPubHash, o2.denomPubHash),
);
- return [denoms, wfPerExchange];
+ return {
+ coinAvailability: denoms,
+ currentWireFeePerExchange: wfPerExchange,
+ };
}
export interface PeerCoinSelectionDetails {
@@ -960,7 +991,9 @@ export interface PeerCoinSelectionDetails {
/**
* How much of the deposit fees is the customer paying?
*/
- depositFees: AmountJson;
+ customerDepositFees: AmountJson;
+
+ totalDepositFees: AmountJson;
maxExpirationDate: TalerProtocolTimestamp;
}
@@ -973,7 +1006,9 @@ export interface ProspectivePeerCoinSelectionDetails {
/**
* How much of the deposit fees is the customer paying?
*/
- depositFees: AmountJson;
+ customerDepositFees: AmountJson;
+
+ totalDepositFees: AmountJson;
maxExpirationDate: TalerProtocolTimestamp;
}
@@ -991,6 +1026,19 @@ export interface PeerCoinSelectionRequest {
instructedAmount: AmountJson;
/**
+ * Are deposit fees covered by the counterparty?
+ *
+ * Defaults to false.
+ */
+ feesCoveredByCounterparty?: boolean;
+
+ /**
+ * Restrict the scope of funds that can be spent via the given
+ * scope info.
+ */
+ restrictScope?: ScopeInfo;
+
+ /**
* Instruct the coin selection to repair this coin
* selection instead of selecting completely new coins.
*/
@@ -1030,17 +1078,21 @@ export async function computeCoinSelMaxExpirationDate(
}
export function emptyTallyForPeerPayment(
- instructedAmount: AmountJson,
+ req: PeerCoinSelectionRequest,
): CoinSelectionTally {
+ const instructedAmount = req.instructedAmount;
const currency = instructedAmount.currency;
const zero = Amounts.zeroOfCurrency(currency);
return {
amountPayRemaining: instructedAmount,
customerDepositFees: zero,
lastDepositFee: zero,
- amountDepositFeeLimitRemaining: zero,
+ amountDepositFeeLimitRemaining: req.feesCoveredByCounterparty
+ ? instructedAmount
+ : zero,
customerWireFees: zero,
wireFeeCoveredForExchange: new Set(),
+ totalDepositFees: zero,
};
}
@@ -1083,7 +1135,7 @@ async function internalSelectPeerCoins(
| undefined
> {
const candidatesRes = await selectPayCandidates(wex, tx, {
- instructedAmount: req.instructedAmount,
+ currency: Amounts.currencyOf(req.instructedAmount),
restrictExchanges: {
auditors: [],
exchanges: [
@@ -1096,11 +1148,11 @@ async function internalSelectPeerCoins(
restrictWireMethod: undefined,
includePendingCoins,
});
- const candidates = candidatesRes[0];
+ const candidates = candidatesRes.coinAvailability;
if (logger.shouldLogTrace()) {
logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
}
- const tally = emptyTallyForPeerPayment(req.instructedAmount);
+ const tally = emptyTallyForPeerPayment(req);
const resCoins: SelectedCoin[] = [];
await maybeRepairCoinSelection(wex, tx, req.repair ?? [], resCoins, tally, {
@@ -1131,17 +1183,142 @@ async function internalSelectPeerCoins(
};
}
-export async function selectPeerCoins(
+export async function selectPeerCoinsInTx(
wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ]
+ >,
req: PeerCoinSelectionRequest,
): Promise<SelectPeerCoinsResult> {
const instructedAmount = req.instructedAmount;
if (Amounts.isZero(instructedAmount)) {
// Other parts of the code assume that we have at least
// one coin to spend.
- throw new Error("amount of zero not allowed");
+ throw new Error("peer-to-peer payment with amount of zero not supported");
+ }
+
+ const exchanges = await tx.exchanges.iter().toArray();
+ const currency = Amounts.currencyOf(instructedAmount);
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ const exchWire = await getExchangeWireDetailsInTx(tx, exch.baseUrl);
+ if (!exchWire) {
+ continue;
+ }
+ const isInScope = req.restrictScope
+ ? await checkExchangeInScope(wex, exch.baseUrl, req.restrictScope)
+ : true;
+ if (!isInScope) {
+ continue;
+ }
+ if (
+ req.restrictScope &&
+ req.restrictScope.type === ScopeType.Exchange &&
+ req.restrictScope.url !== exch.baseUrl
+ ) {
+ continue;
+ }
+ const globalFees = getGlobalFees(exchWire);
+ if (!globalFees) {
+ continue;
+ }
+
+ const avRes = await internalSelectPeerCoins(wex, tx, req, exchWire, false);
+
+ if (!avRes) {
+ // Try to see if we can do a prospective selection
+ const prospectiveAvRes = await internalSelectPeerCoins(
+ wex,
+ tx,
+ req,
+ exchWire,
+ true,
+ );
+ if (prospectiveAvRes) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvRes.sel)) {
+ const mySel = prospectiveAvRes.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ prospectiveAvRes.sel,
+ );
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ customerDepositFees: prospectiveAvRes.tally.customerDepositFees,
+ totalDepositFees: prospectiveAvRes.tally.totalDepositFees,
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
+ };
+ }
+ } else if (avRes) {
+ const r = await assembleSelectPayCoinsSuccessResult(
+ tx,
+ avRes.sel,
+ avRes.resCoins,
+ avRes.tally,
+ );
+
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ avRes.sel,
+ );
+
+ return {
+ type: "success",
+ result: {
+ coins: r.coins,
+ customerDepositFees: Amounts.parseOrThrow(r.customerDepositFees),
+ totalDepositFees: Amounts.parseOrThrow(r.totalDepositFees),
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
+ };
+ }
}
+ const insufficientBalanceDetails = await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: undefined,
+ instructedAmount: req.instructedAmount,
+ requiredMinimumAge: undefined,
+ wireMethod: undefined,
+ depositPaytoUri: undefined,
+ },
+ );
+ return {
+ type: "failure",
+ insufficientBalanceDetails,
+ };
+}
+export async function selectPeerCoins(
+ wex: WalletExecutionContext,
+ req: PeerCoinSelectionRequest,
+): Promise<SelectPeerCoinsResult> {
return await wex.db.runReadWriteTx(
{
storeNames: [
@@ -1155,8 +1332,137 @@ export async function selectPeerCoins(
],
},
async (tx): Promise<SelectPeerCoinsResult> => {
+ return selectPeerCoinsInTx(wex, tx, req);
+ },
+ );
+}
+
+function getMaxDepositAmountForAvailableCoins(
+ req: GetMaxDepositAmountRequest,
+ candidateRes: PayCoinCandidates,
+): GetMaxDepositAmountResponse {
+ const wireFeeCoveredForExchange = new Set<string>();
+
+ let amountEffective = Amounts.zeroOfCurrency(req.currency);
+ let fees = Amounts.zeroOfCurrency(req.currency);
+
+ for (const cc of candidateRes.coinAvailability) {
+ if (!wireFeeCoveredForExchange.has(cc.exchangeBaseUrl)) {
+ const wireFee =
+ candidateRes.currentWireFeePerExchange[cc.exchangeBaseUrl];
+ // Wire fee can be null if max deposit amount is computed
+ // without restricting the wire method.
+ if (wireFee != null) {
+ fees = Amounts.add(fees, wireFee).amount;
+ }
+ wireFeeCoveredForExchange.add(cc.exchangeBaseUrl);
+ }
+
+ amountEffective = Amounts.add(
+ amountEffective,
+ Amounts.mult(cc.value, cc.numAvailable).amount,
+ ).amount;
+
+ fees = Amounts.add(
+ fees,
+ Amounts.mult(cc.feeDeposit, cc.numAvailable).amount,
+ ).amount;
+ }
+
+ return {
+ effectiveAmount: Amounts.stringify(amountEffective),
+ rawAmount: Amounts.stringify(Amounts.sub(amountEffective, fees).amount),
+ };
+}
+
+/**
+ * Only used for unit testing getMaxDepositAmountForAvailableCoins.
+ */
+export const testing_getMaxDepositAmountForAvailableCoins =
+ getMaxDepositAmountForAvailableCoins;
+
+export async function getMaxDepositAmount(
+ wex: WalletExecutionContext,
+ req: GetMaxDepositAmountRequest,
+): Promise<GetMaxDepositAmountResponse> {
+ logger.trace(`getting max deposit amount for: ${j2s(req)}`);
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "coinAvailability",
+ "denominations",
+ "exchangeDetails",
+ ],
+ },
+ async (tx): Promise<GetMaxDepositAmountResponse> => {
+ let restrictWireMethod: string | undefined = undefined;
+ if (req.depositPaytoUri) {
+ const p = parsePaytoUri(req.depositPaytoUri);
+ if (!p) {
+ throw Error("invalid payto URI");
+ }
+ restrictWireMethod = p.targetType;
+ }
+ const candidateRes = await selectPayCandidates(wex, tx, {
+ currency: req.currency,
+ restrictExchanges: undefined,
+ restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ requiredMinimumAge: undefined,
+ includePendingCoins: true,
+ });
+ return getMaxDepositAmountForAvailableCoins(req, candidateRes);
+ },
+ );
+}
+
+function getMaxPeerPushDebitAmountForAvailableCoins(
+ req: GetMaxDepositAmountRequest,
+ exchangeBaseUrl: string,
+ candidateRes: PayCoinCandidates,
+): GetMaxPeerPushDebitAmountResponse {
+ let amountEffective = Amounts.zeroOfCurrency(req.currency);
+ let fees = Amounts.zeroOfCurrency(req.currency);
+
+ for (const cc of candidateRes.coinAvailability) {
+ amountEffective = Amounts.add(
+ amountEffective,
+ Amounts.mult(cc.value, cc.numAvailable).amount,
+ ).amount;
+
+ fees = Amounts.add(
+ fees,
+ Amounts.mult(cc.feeDeposit, cc.numAvailable).amount,
+ ).amount;
+ }
+
+ return {
+ exchangeBaseUrl,
+ effectiveAmount: Amounts.stringify(amountEffective),
+ rawAmount: Amounts.stringify(Amounts.sub(amountEffective, fees).amount),
+ };
+}
+
+export async function getMaxPeerPushDebitAmount(
+ wex: WalletExecutionContext,
+ req: GetMaxPeerPushDebitAmountRequest,
+): Promise<GetMaxPeerPushDebitAmountResponse> {
+ logger.trace(`getting max deposit amount for: ${j2s(req)}`);
+
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "coinAvailability",
+ "denominations",
+ "exchangeDetails",
+ ],
+ },
+ async (tx): Promise<GetMaxPeerPushDebitAmountResponse> => {
+ let result: GetMaxDepositAmountResponse | undefined = undefined;
+ const currency = req.currency;
const exchanges = await tx.exchanges.iter().toArray();
- const currency = Amounts.currencyOf(instructedAmount);
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== currency) {
continue;
@@ -1165,95 +1471,58 @@ export async function selectPeerCoins(
if (!exchWire) {
continue;
}
+ const isInScope = req.restrictScope
+ ? await checkExchangeInScope(wex, exch.baseUrl, req.restrictScope)
+ : true;
+ if (!isInScope) {
+ continue;
+ }
+ if (
+ req.restrictScope &&
+ req.restrictScope.type === ScopeType.Exchange &&
+ req.restrictScope.url !== exch.baseUrl
+ ) {
+ continue;
+ }
const globalFees = getGlobalFees(exchWire);
if (!globalFees) {
continue;
}
- const avRes = await internalSelectPeerCoins(
- wex,
- tx,
- req,
- exchWire,
- false,
- );
-
- if (!avRes) {
- // Try to see if we can do a prospective selection
- const prospectiveAvRes = await internalSelectPeerCoins(
- wex,
- tx,
- req,
- exchWire,
- true,
- );
- if (prospectiveAvRes) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvRes.sel)) {
- const mySel = prospectiveAvRes.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- const maxExpirationDate = await computeCoinSelMaxExpirationDate(
- wex,
- tx,
- prospectiveAvRes.sel,
- );
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- depositFees: prospectiveAvRes.tally.customerDepositFees,
- exchangeBaseUrl: exch.baseUrl,
- maxExpirationDate,
+ const candidatesRes = await selectPayCandidates(wex, tx, {
+ currency,
+ restrictExchanges: {
+ auditors: [],
+ exchanges: [
+ {
+ exchangeBaseUrl: exchWire.exchangeBaseUrl,
+ exchangePub: exchWire.masterPublicKey,
},
- };
- }
- } else if (avRes) {
- const r = await assembleSelectPayCoinsSuccessResult(
- tx,
- avRes.sel,
- avRes.resCoins,
- avRes.tally,
- );
+ ],
+ },
+ restrictWireMethod: undefined,
+ includePendingCoins: true,
+ });
- const maxExpirationDate = await computeCoinSelMaxExpirationDate(
- wex,
- tx,
- avRes.sel,
- );
+ const myExchangeRes = getMaxPeerPushDebitAmountForAvailableCoins(
+ req,
+ exchWire.exchangeBaseUrl,
+ candidatesRes,
+ );
- return {
- type: "success",
- result: {
- coins: r.coins,
- depositFees: Amounts.parseOrThrow(r.customerDepositFees),
- exchangeBaseUrl: exch.baseUrl,
- maxExpirationDate,
- },
- };
+ if (!result) {
+ result = myExchangeRes;
+ } else if (Amounts.cmp(result.rawAmount, myExchangeRes.rawAmount) < 0) {
+ result = myExchangeRes;
}
}
- const insufficientBalanceDetails = await reportInsufficientBalanceDetails(
- wex,
- tx,
- {
- restrictExchanges: undefined,
- instructedAmount: req.instructedAmount,
- requiredMinimumAge: undefined,
- wireMethod: undefined,
- depositPaytoUri: undefined,
- },
- );
- return {
- type: "failure",
- insufficientBalanceDetails,
- };
+ if (!result) {
+ return {
+ effectiveAmount: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
+ rawAmount: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
+ };
+ }
+ return result;
},
);
}
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index a20278cf3..d9438d19c 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -30,21 +30,25 @@ import {
ExchangeTosStatus,
ExchangeUpdateStatus,
Logger,
+ ObservabilityEventType,
RefreshReason,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TombstoneIdStr,
+ Transaction,
TransactionIdStr,
WalletNotification,
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
durationMul,
- j2s,
} from "@gnu-taler/taler-util";
import {
BackupProviderRecord,
+ CoinHistoryRecord,
CoinRecord,
DbPreciseTimestamp,
DepositGroupRecord,
@@ -58,10 +62,12 @@ import {
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
+ WalletDbAllStoresReadOnlyTransaction,
WalletDbReadWriteTransaction,
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
+import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -72,9 +78,9 @@ export interface CoinsSpendInfo {
contributions: AmountJson[];
refreshReason: RefreshReason;
/**
- * Identifier for what the coin has been spent for.
+ * Transaction for which the coin is spent.
*/
- allocationId: TransactionIdStr;
+ transactionId: TransactionIdStr;
}
export async function makeCoinsVisible(
@@ -121,7 +127,10 @@ export async function makeCoinAvailable(
coinRecord.exchangeBaseUrl,
coinRecord.denomPubHash,
]);
- checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinRecord.denomPubHash}`,
+ );
const ageRestriction = coinRecord.maxAge;
let car = await tx.coinAvailability.get([
coinRecord.exchangeBaseUrl,
@@ -144,15 +153,21 @@ export async function makeCoinAvailable(
await tx.coinAvailability.put(car);
}
+/**
+ * Spend coins. Marks the coins as used, adds a coin history items
+ * and creates refresh group.
+ */
export async function spendCoins(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
[
"coins",
+ "coinHistory",
"coinAvailability",
"refreshGroups",
"refreshSessions",
"denominations",
+ "transactionsMeta",
]
>,
csi: CoinsSpendInfo,
@@ -175,36 +190,21 @@ export async function spendCoins(
coin.exchangeBaseUrl,
coin.denomPubHash,
);
- checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coin.denomPubHash}`);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coin.denomPubHash}`,
+ );
const coinAvailability = await tx.coinAvailability.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
coin.maxAge,
]);
- checkDbInvariant(!!coinAvailability, `age denom info is missing for ${coin.maxAge}`);
+ checkDbInvariant(
+ !!coinAvailability,
+ `age denom info is missing for ${coin.maxAge}`,
+ );
const contrib = csi.contributions[i];
- if (coin.status !== CoinStatus.Fresh) {
- const alloc = coin.spendAllocation;
- if (!alloc) {
- continue;
- }
- if (alloc.id !== csi.allocationId) {
- // FIXME: assign error code
- logger.info("conflicting coin allocation ID");
- logger.info(`old ID: ${alloc.id}, new ID: ${csi.allocationId}`);
- throw Error("conflicting coin allocation (id)");
- }
- if (0 !== Amounts.cmp(alloc.amount, contrib)) {
- // FIXME: assign error code
- throw Error("conflicting coin allocation (contrib)");
- }
- continue;
- }
coin.status = CoinStatus.Dormant;
- coin.spendAllocation = {
- id: csi.allocationId,
- amount: Amounts.stringify(contrib),
- };
const remaining = Amounts.sub(denom.value, contrib);
if (remaining.saturated) {
throw Error("not enough remaining balance on coin for payment");
@@ -226,6 +226,21 @@ export async function spendCoins(
coinAvailability.visibleCoinCount--;
}
}
+ let histEntry: CoinHistoryRecord | undefined = await tx.coinHistory.get(
+ coin.coinPub,
+ );
+ if (!histEntry) {
+ histEntry = {
+ coinPub: coin.coinPub,
+ history: [],
+ };
+ }
+ histEntry.history.push({
+ type: "spend",
+ transactionId: csi.transactionId,
+ amount: Amounts.stringify(contrib),
+ });
+ await tx.coinHistory.put(histEntry);
await tx.coins.put(coin);
await tx.coinAvailability.put(coinAvailability);
}
@@ -236,7 +251,7 @@ export async function spendCoins(
Amounts.currencyOf(csi.contributions[0]),
refreshCoinPubs,
csi.refreshReason,
- csi.allocationId,
+ csi.transactionId,
);
}
@@ -257,6 +272,9 @@ export enum TombstoneTag {
export function getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
+ if (exchange.tosCurrentEtag == null) {
+ return ExchangeTosStatus.MissingTos;
+ }
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
@@ -282,6 +300,8 @@ export function getExchangeUpdateStatusFromRecord(
return ExchangeUpdateStatus.ReadyUpdate;
case ExchangeEntryDbUpdateStatus.Suspended:
return ExchangeUpdateStatus.Suspended;
+ case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
+ return ExchangeUpdateStatus.OutdatedUpdate;
default:
assertUnreachable(r.updateStatus);
}
@@ -364,6 +384,7 @@ export enum TaskRunResultType {
Error = "error",
LongpollReturnedPending = "longpoll-returned-pending",
ScheduleLater = "schedule-later",
+ NetworkRequired = "network-required",
}
export type TaskRunResult =
@@ -372,7 +393,8 @@ export type TaskRunResult =
| TaskRunBackoffResult
| TaskRunProgressResult
| TaskRunLongpollReturnedPendingResult
- | TaskRunScheduleLaterResult;
+ | TaskRunScheduleLaterResult
+ | TaskRunNetworkRequiredResult;
export namespace TaskRunResult {
/**
@@ -419,6 +441,15 @@ export namespace TaskRunResult {
type: TaskRunResultType.LongpollReturnedPending,
};
}
+ /**
+ * Network connection is required to complete the task.
+ * When network is back, the transaction will be retried.
+ */
+ export function networkRequired(): TaskRunResult {
+ return {
+ type: TaskRunResultType.NetworkRequired,
+ };
+ }
}
export interface TaskRunFinishedResult {
@@ -442,6 +473,10 @@ export interface TaskRunLongpollReturnedPendingResult {
type: TaskRunResultType.LongpollReturnedPending;
}
+export interface TaskRunNetworkRequiredResult {
+ type: TaskRunResultType.NetworkRequired;
+}
+
export interface TaskRunErrorResult {
type: TaskRunResultType.Error;
errorDetail: TalerErrorDetail;
@@ -565,6 +600,7 @@ export function getAutoRefreshExecuteThreshold(d: {
export enum PendingTaskType {
ExchangeUpdate = "exchange-update",
+ ExchangeWalletKyc = "exchange-wallet-kyc",
Purchase = "purchase",
Refresh = "refresh",
Recoup = "recoup",
@@ -587,6 +623,7 @@ export type ParsedTaskIdentifier =
withdrawalGroupId: string;
}
| { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+ | { tag: PendingTaskType.ExchangeWalletKyc; exchangeBaseUrl: string }
| { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
| { tag: PendingTaskType.Deposit; depositGroupId: string }
| { tag: PendingTaskType.PeerPullDebit; peerPullDebitId: string }
@@ -613,6 +650,8 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
return { tag: type, depositGroupId: rest[0] };
case PendingTaskType.ExchangeUpdate:
return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
+ case PendingTaskType.ExchangeWalletKyc:
+ return { tag: type, exchangeBaseUrl: decodeURIComponent(rest[0]) };
case PendingTaskType.PeerPullCredit:
return { tag: type, pursePub: rest[0] };
case PendingTaskType.PeerPullDebit:
@@ -644,6 +683,8 @@ export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskIdStr {
return `${p.tag}:${p.depositGroupId}` as TaskIdStr;
case PendingTaskType.ExchangeUpdate:
return `${p.tag}:${encodeURIComponent(p.exchangeBaseUrl)}` as TaskIdStr;
+ case PendingTaskType.ExchangeWalletKyc:
+ return `${p.tag}:${encodeURIComponent(p.exchangeBaseUrl)}` as TaskIdStr;
case PendingTaskType.PeerPullDebit:
return `${p.tag}:${p.peerPullDebitId}` as TaskIdStr;
case PendingTaskType.PeerPushCredit:
@@ -758,11 +799,14 @@ export const TransitionResult = {
export interface TransactionContext {
get taskId(): TaskIdStr | undefined;
get transactionId(): TransactionIdStr;
- abortTransaction(): Promise<void>;
+ abortTransaction(reason?: TalerErrorDetail): Promise<void>;
suspendTransaction(): Promise<void>;
resumeTransaction(): Promise<void>;
- failTransaction(): Promise<void>;
+ failTransaction(reason?: TalerErrorDetail): Promise<void>;
deleteTransaction(): Promise<void>;
+ lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined>;
}
declare const __taskIdStr: unique symbol;
@@ -818,3 +862,123 @@ export async function genericWaitForState(
throw e;
}
}
+
+/**
+ * Wait until the wallet is in a particular state.
+ *
+ * Two functions must be provided:
+ * 1. checkState, which checks if the wallet is in the
+ * desired state.
+ * 2. filterNotification, which checks whether a notification
+ * might have lead to a state change.
+ */
+export async function genericWaitForStateVal<T>(
+ wex: WalletExecutionContext,
+ args: {
+ checkState: () => Promise<T | undefined>;
+ filterNotification: (notif: WalletNotification) => boolean;
+ },
+): Promise<T> {
+ await wex.taskScheduler.ensureRunning();
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const flag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (args.filterNotification(notif)) {
+ flag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
+ cancelNotif();
+ flag.raise();
+ });
+
+ try {
+ while (true) {
+ if (wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+ const val = await args.checkState();
+ if (val != null) {
+ return val;
+ }
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ throw e;
+ }
+}
+
+export function requireExchangeTosAcceptedOrThrow(
+ exchange: ReadyExchangeSummary,
+): void {
+ switch (exchange.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ case ExchangeTosStatus.MissingTos:
+ break;
+ default:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ currentEtag: exchange.tosCurrentEtag,
+ tosStatus: exchange.tosStatus,
+ },
+ );
+ }
+}
+
+/**
+ * Run a request, but cancel the wallet execution context as soon as the client
+ * submits another request of the same type.
+ */
+export async function runWithClientCancellation<R, T>(
+ wex: WalletExecutionContext,
+ operation: string,
+ clientCancellationId: string | undefined,
+ handler: () => Promise<T>,
+): Promise<T> {
+ const clientCancelKey = clientCancellationId
+ ? `ccid:${operation}:${clientCancellationId}`
+ : undefined;
+ const cts = wex.cts;
+ if (clientCancelKey && cts) {
+ const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey);
+ if (prevCts) {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Cancelling previous key ${clientCancelKey}`,
+ });
+ prevCts.cancel(
+ `cancelled by subsequent request with same cancellation ID`,
+ );
+ } else {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `No previous key ${clientCancelKey}`,
+ });
+ }
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ wex.ws.clientCancellationMap.set(clientCancelKey, cts);
+ }
+ try {
+ return await handler();
+ } finally {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ if (clientCancelKey && wex.cts && !wex.cts.token.isCancelled) {
+ wex.ws.clientCancellationMap.delete(clientCancelKey);
+ }
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 2a2958a71..cd5088fc6 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -114,6 +114,10 @@ import {
SignReservePurseCreateRequest,
SignReservePurseCreateResponse,
SignTrackTransactionRequest,
+ SignWalletAccountSetupRequest,
+ SignWalletAccountSetupResponse,
+ SignWalletKycAuthRequest,
+ SignWalletKycAuthResponse,
} from "./cryptoTypes.js";
const logger = new Logger("cryptoImplementation.ts");
@@ -166,7 +170,7 @@ export interface TalerCryptoInterface {
req: ContractTermsValidationRequest,
): Promise<ValidationResult>;
- createEddsaKeypair(req: {}): Promise<EddsaKeypair>;
+ createEddsaKeypair(req: {}): Promise<EddsaKeyPairStrings>;
eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;
@@ -240,6 +244,10 @@ export interface TalerCryptoInterface {
signPurseMerge(req: SignPurseMergeRequest): Promise<SignPurseMergeResponse>;
+ signWalletAccountSetup(
+ req: SignWalletAccountSetupRequest,
+ ): Promise<SignWalletAccountSetupResponse>;
+
signReservePurseCreate(
req: SignReservePurseCreateRequest,
): Promise<SignReservePurseCreateResponse>;
@@ -253,6 +261,10 @@ export interface TalerCryptoInterface {
signCoinHistoryRequest(
req: SignCoinHistoryRequest,
): Promise<SignCoinHistoryResponse>;
+
+ signWalletKycAuth(
+ req: SignWalletKycAuthRequest,
+ ): Promise<SignWalletKycAuthResponse>;
}
/**
@@ -321,10 +333,12 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
- createEddsaKeypair: function (req: unknown): Promise<EddsaKeypair> {
+ createEddsaKeypair: function (req: unknown): Promise<EddsaKeyPairStrings> {
throw new Error("Function not implemented.");
},
- eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> {
+ eddsaGetPublic: function (
+ req: EddsaGetPublicRequest,
+ ): Promise<EddsaKeyPairStrings> {
throw new Error("Function not implemented.");
},
unblindDenominationSignature: function (
@@ -447,6 +461,16 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignReserveHistoryReqResponse> {
throw new Error("Function not implemented.");
},
+ signWalletAccountSetup: function (
+ req: SignWalletAccountSetupRequest,
+ ): Promise<SignWalletAccountSetupResponse> {
+ throw new Error("Function not implemented.");
+ },
+ signWalletKycAuth: function (
+ req: SignWalletKycAuthRequest,
+ ): Promise<SignWalletKycAuthResponse> {
+ throw new Error("Function not implemented.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -497,6 +521,7 @@ export interface SpendCoinDetails {
coinPub: string;
coinPriv: string;
contribution: AmountString;
+ feeDeposit: AmountString;
denomPubHash: string;
denomSig: UnblindedSignature;
ageCommitmentProof: AgeCommitmentProof | undefined;
@@ -578,7 +603,7 @@ export interface WireAccountValidationRequest {
creditRestrictions?: any[];
}
-export interface EddsaKeypair {
+export interface EddsaKeyPairStrings {
priv: string;
pub: string;
}
@@ -1059,7 +1084,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
/**
* Create a new EdDSA key pair.
*/
- async createEddsaKeypair(tci: TalerCryptoInterfaceR): Promise<EddsaKeypair> {
+ async createEddsaKeypair(
+ tci: TalerCryptoInterfaceR,
+ ): Promise<EddsaKeyPairStrings> {
const eddsaPriv = encodeCrock(getRandomBytes(32));
const eddsaPubRes = await tci.eddsaGetPublic(tci, {
priv: eddsaPriv,
@@ -1073,7 +1100,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
async eddsaGetPublic(
tci: TalerCryptoInterfaceR,
req: EddsaGetPublicRequest,
- ): Promise<EddsaKeypair> {
+ ): Promise<EddsaKeyPairStrings> {
return {
priv: req.priv,
pub: encodeCrock(eddsaGetPublic(decodeCrock(req.priv))),
@@ -1765,6 +1792,34 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
sig: sigResp.sig,
};
},
+ async signWalletAccountSetup(
+ tci: TalerCryptoInterfaceR,
+ req: SignWalletAccountSetupRequest,
+ ): Promise<SignWalletAccountSetupResponse> {
+ const sigData = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
+ .put(amountToBuffer(req.threshold))
+ .build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(sigData),
+ priv: req.reservePriv,
+ });
+ return {
+ sig: sigResp.sig,
+ };
+ },
+ async signWalletKycAuth(
+ tci: TalerCryptoInterfaceR,
+ req: SignWalletKycAuthRequest,
+ ): Promise<SignWalletKycAuthResponse> {
+ const sigData = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(sigData),
+ priv: req.accountPriv,
+ });
+ return {
+ sig: sigResp.sig,
+ };
+ },
};
export interface EddsaSignRequest {
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index df25b87e4..f6d17cb51 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -225,6 +225,25 @@ export interface DecryptContractForDepositResponse {
contractTerms: any;
}
+export interface SignWalletAccountSetupRequest {
+ reservePub: string;
+ reservePriv: string;
+ threshold: AmountString;
+}
+
+export interface SignWalletAccountSetupResponse {
+ sig: string;
+}
+
+export interface SignWalletKycAuthRequest {
+ accountPub: string;
+ accountPriv: string;
+}
+
+export interface SignWalletKycAuthResponse {
+ sig: string;
+}
+
export interface SignPurseMergeRequest {
mergeTimestamp: TalerProtocolTimestamp;
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/taler-wallet-core/src/crypto/index.ts
index 84cc95e72..2c4c3fcb3 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
+++ b/packages/taler-wallet-core/src/crypto/index.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
+ (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
@@ -14,13 +14,12 @@
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 { CryptoDispatcher } from "./workers/crypto-dispatcher.js";
+import { SynchronousCryptoWorkerFactoryPlain } from "./workers/synchronousWorkerFactoryPlain.js";
-export default function UpdateTransfer(): VNode {
- return <div>order transfer page</div>;
+export function createSyncCryptoApi() {
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ return cryptiDisp.cryptoApi;
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 5c381eea7..8f3eda3b6 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -29,6 +29,7 @@ import {
} from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
+ AccountLimit,
AgeCommitmentProof,
AmountString,
Amounts,
@@ -40,6 +41,7 @@ import {
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DenomLossEventType,
DenomSelectionState,
DenominationInfo,
@@ -51,16 +53,19 @@ import {
HashCodeString,
Logger,
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
- Transaction,
TransactionIdStr,
UnblindedSignature,
WireInfo,
WithdrawalExchangeAccountDetails,
+ ZeroLimitedOperation,
codecForAny,
+ j2s,
+ stringifyScopeInfo,
} from "@gnu-taler/taler-util";
import { DbRetryInfo, TaskIdentifiers } from "./common.js";
import {
@@ -151,7 +156,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 10;
+export const WALLET_DB_MINOR_VERSION = 13;
declare const symDbProtocolTimestamp: unique symbol;
@@ -301,28 +306,37 @@ export enum WithdrawalGroupStatus {
SuspendedReady = 0x0110_0004,
/**
- * Proposed to the user, has can choose to accept/refuse.
+ * Exchange wants KYC info from the user.
*/
- DialogProposed = 0x0101_0000,
+ PendingKyc = 0x0100_0005,
+ SuspendedKyc = 0x0110_005,
/**
- * We are telling the bank that we don't want to complete
- * the withdrawal!
+ * Exchange wants KYC info from the user.
+ * KYC link is ready.
*/
- AbortingBank = 0x0103_0001,
- SuspendedAbortingBank = 0x0113_0001,
+ PendingBalanceKyc = 0x0100_0006,
+ SuspendedBalanceKyc = 0x0110_006,
/**
* Exchange wants KYC info from the user.
+ *
+ * KYC link is not ready yet, the KYC process is still initializing.
*/
- PendingKyc = 0x0100_0005,
- SuspendedKyc = 0x0110_005,
+ PendingBalanceKycInit = 0x0100_0007,
+ SuspendedBalanceKycInit = 0x0110_007,
/**
- * Exchange is doing AML checks.
+ * Proposed to the user, has can choose to accept/refuse.
+ */
+ DialogProposed = 0x0101_0000,
+
+ /**
+ * We are telling the bank that we don't want to complete
+ * the withdrawal!
*/
- PendingAml = 0x0100_0006,
- SuspendedAml = 0x0110_0006,
+ AbortingBank = 0x0103_0001,
+ SuspendedAbortingBank = 0x0113_0001,
/**
* The corresponding withdraw record has been created.
@@ -400,6 +414,10 @@ export interface ReserveBankInfo {
wireTypes: string[] | undefined;
currency: string | undefined;
+
+ externalConfirmation?: boolean;
+
+ senderWire?: string;
}
/**
@@ -608,6 +626,12 @@ export interface ExchangeDetailsRecord {
* Age restrictions supported by the exchange (bitmask).
*/
ageMask?: number;
+
+ walletBalanceLimits?: AmountString[];
+
+ hardLimits?: AccountLimit[];
+
+ zeroLimits?: ZeroLimitedOperation[];
}
export interface ExchangeDetailsPointer {
@@ -637,6 +661,7 @@ export enum ExchangeEntryDbUpdateStatus {
// Reserved 5 for backwards compatibility.
Ready = 6,
ReadyUpdate = 7,
+ OutdatedUpdate = 8,
}
/**
@@ -677,6 +702,8 @@ export interface ExchangeEntryRecord {
updateStatus: ExchangeEntryDbUpdateStatus;
+ unavailableReason?: TalerErrorDetail;
+
/**
* If set to true, the next update to the exchange
* status will request /keys with no-cache headers set.
@@ -702,6 +729,14 @@ export interface ExchangeEntryRecord {
*/
nextUpdateStamp: DbPreciseTimestamp;
+ /**
+ * The number of times we tried to contact the exchange,
+ * the exchange returned a result, but it is conflicting with the
+ * existing exchange entry.
+ *
+ * We keep the retry counter here instead of using the task retries,
+ * as the task succeeded, the exchange is just not usable.
+ */
updateRetryCounter?: number;
lastKeysEtag: string | undefined;
@@ -894,15 +929,6 @@ export interface CoinRecord {
visible?: number;
/**
- * Information about what the coin has been allocated for.
- *
- * Used for:
- * - Diagnostics
- * - Idempotency of applying a coin selection (e.g. after re-selection)
- */
- spendAllocation: CoinAllocation | undefined;
-
- /**
* Maximum age of purchases that can be made with this coin.
*
* (Used for indexing, redundant with {@link ageCommitmentProof}).
@@ -913,14 +939,50 @@ export interface CoinRecord {
}
/**
- * Coin allocation, i.e. what a coin has been used for.
+ * History item for a coin.
+ *
+ * DB-specific format,
*/
-export interface CoinAllocation {
+export type DbWalletCoinHistoryItem =
+ | {
+ type: "withdraw";
+ transactionId: TransactionIdStr;
+ }
+ | {
+ type: "spend";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "refresh";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "recoup";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ }
+ | {
+ type: "refund";
+ transactionId: TransactionIdStr;
+ amount: AmountString;
+ };
+
+/**
+ * History event for a coin from the wallet's perspective.
+ *
+ * The history might reference transactions that were already deleted from the wallet.
+ */
+export interface CoinHistoryRecord {
+ coinPub: string;
/**
- * ID of the allocation, should be the ID of the transaction that
+ * History items for the coin.
+ *
+ * We store this as an array in the object store, as the coin history
+ * is pretty much always very small.
*/
- id: TransactionIdStr;
- amount: AmountString;
+ history: DbWalletCoinHistoryItem[];
}
export enum RefreshCoinStatus {
@@ -1008,6 +1070,8 @@ export interface RefreshGroupRecord {
timestampCreated: DbPreciseTimestamp;
+ failReason?: TalerErrorDetail;
+
/**
* Timestamp when the refresh session finished.
*/
@@ -1225,6 +1289,9 @@ export interface PurchaseRecord {
purchaseStatus: PurchaseStatus;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
+
/**
* Private key for the nonce.
*/
@@ -1310,6 +1377,7 @@ export enum ConfigRecordKey {
// Only for testing, do not use!
TestLoopTx = "testTxLoop",
LastInitInfo = "lastInitInfo",
+ MaterializedTransactionsVersion = "materializedTransactionsVersion",
}
/**
@@ -1323,7 +1391,8 @@ export type ConfigRecord =
}
| { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean }
| { key: ConfigRecordKey.TestLoopTx; value: number }
- | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp };
+ | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp }
+ | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number };
export interface WalletBackupConfState {
deviceId: string;
@@ -1358,6 +1427,7 @@ export const enum WithdrawalRecordType {
export interface WgInfoBankIntegrated {
withdrawalType: WithdrawalRecordType.BankIntegrated;
+
/**
* Extra state for when this is a withdrawal involving
* a Taler-integrated bank.
@@ -1410,10 +1480,6 @@ export type WgInfo =
export type KycUserType = "individual" | "business";
-export interface KycPendingInfo {
- paytoHash: string;
- requirementRow: number;
-}
/**
* Group of withdrawal operations that need to be executed.
* (Either for a normal withdrawal or from a reward.)
@@ -1429,9 +1495,9 @@ export interface WithdrawalGroupRecord {
wgInfo: WgInfo;
- kycPending?: KycPendingInfo;
+ kycPaytoHash?: string;
- kycUrl?: string;
+ kycAccessToken?: string;
/**
* Secret seed used to derive planchets.
@@ -1474,14 +1540,6 @@ export interface WithdrawalGroupRecord {
status: WithdrawalGroupStatus;
/**
- * Wire information (as payto URI) for the bank account that
- * transferred funds for this reserve.
- *
- * FIXME: Doesn't this belong to the bankAccounts object store?
- */
- senderWire?: string;
-
- /**
* Restrict withdrawals from this reserve to this age.
*/
restrictAge?: number;
@@ -1529,6 +1587,9 @@ export interface WithdrawalGroupRecord {
* FIXME: Should this not also include a timestamp for more logical merging?
*/
denomSelUid: string;
+
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
}
export interface BankWithdrawUriRecord {
@@ -1673,20 +1734,30 @@ export interface BackupProviderRecord {
export enum DepositOperationStatus {
PendingDeposit = 0x0100_0000,
+ SuspendedDeposit = 0x0110_0000,
+
PendingTrack = 0x0100_0001,
- PendingKyc = 0x0100_0002,
+ SuspendedTrack = 0x0110_0001,
- Aborting = 0x0103_0000,
+ PendingAggregateKyc = 0x0100_0002,
+ SuspendedAggregateKyc = 0x0110_0002,
- SuspendedDeposit = 0x0110_0000,
- SuspendedTrack = 0x0110_0001,
- SuspendedKyc = 0x0110_0002,
+ PendingDepositKyc = 0x0100_0003,
+ SuspendedDepositKyc = 0x0110_0003,
+
+ PendingDepositKycAuth = 0x0100_0005,
+ SuspendedDepositKycAuth = 0x0110_0005,
+ Aborting = 0x0103_0000,
SuspendedAborting = 0x0113_0000,
Finished = 0x0500_0000,
- Failed = 0x0501_0000,
- Aborted = 0x0503_0000,
+
+ FailedDeposit = 0x0501_0000,
+
+ FailedTrack = 0x0501_0001,
+
+ AbortedDeposit = 0x0503_0000,
}
export interface DepositTrackingInfo {
@@ -1770,6 +1841,9 @@ export interface DepositGroupRecord {
*/
abortRefreshGroupId?: string;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
+
kycInfo?: DepositKycInfo;
// FIXME: Do we need this and should it be in this object store?
@@ -1779,8 +1853,7 @@ export interface DepositGroupRecord {
}
export interface DepositKycInfo {
- kycUrl: string;
- requirementRow: number;
+ accessToken?: string;
paytoHash: string;
exchangeBaseUrl: string;
}
@@ -1835,10 +1908,22 @@ export interface PeerPushDebitRecord {
exchangeBaseUrl: string;
/**
+ * Restricted scope for this transaction.
+ *
+ * Relevant for coin reselection.
+ */
+ restrictScope?: ScopeInfo;
+
+ /**
* Instructed amount.
*/
amount: AmountString;
+ /**
+ * Effective amount.
+ *
+ * (Called totalCost for historical reasons.)
+ */
totalCost: AmountString;
coinSel?: DbPeerPushPaymentCoinSelection;
@@ -1880,6 +1965,9 @@ export interface PeerPushDebitRecord {
abortRefreshGroupId?: string;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
+
/**
* Status of the peer push payment initiation.
*/
@@ -1887,22 +1975,33 @@ export interface PeerPushDebitRecord {
}
export enum PeerPullPaymentCreditStatus {
+ /**
+ * Typically the initial state of the peer-pull-credit transaction,
+ * purse will be created.
+ */
PendingCreatePurse = 0x0100_0000,
+ SuspendedCreatePurse = 0x0110_0000,
+
/**
* Purse created, waiting for the other party to accept the
* invoice and deposit money into it.
*/
PendingReady = 0x0100_0001,
+ SuspendedReady = 0x0110_0001,
+
PendingMergeKycRequired = 0x0100_0002,
+ SuspendedMergeKycRequired = 0x0110_0002,
+
PendingWithdrawing = 0x0100_0003,
+ SuspendedWithdrawing = 0x0110_0003,
- AbortingDeletePurse = 0x0103_0000,
+ PendingBalanceKycRequired = 0x0100_0004,
+ SuspendedBalanceKycRequired = 0x0110_0004,
- SuspendedCreatePurse = 0x0110_0000,
- SuspendedReady = 0x0110_0001,
- SuspendedMergeKycRequired = 0x0110_0002,
- SuspendedWithdrawing = 0x0110_0003,
+ PendingBalanceKycInit = 0x0100_0005,
+ SuspendedBalanceKycInit = 0x0110_0005,
+ AbortingDeletePurse = 0x0103_0000,
SuspendedAbortingDeletePurse = 0x0113_0000,
Done = 0x0500_0000,
@@ -1959,26 +2058,36 @@ export interface PeerPullCreditRecord {
*/
status: PeerPullPaymentCreditStatus;
- kycInfo?: KycPendingInfo;
+ kycPaytoHash?: string;
+
+ kycAccessToken?: string;
- kycUrl?: string;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
withdrawalGroupId: string | undefined;
}
export enum PeerPushCreditStatus {
PendingMerge = 0x0100_0000,
+ SuspendedMerge = 0x0110_0000,
+
PendingMergeKycRequired = 0x0100_0001,
+ SuspendedMergeKycRequired = 0x0110_0001,
+
/**
* Merge was successful and withdrawal group has been created, now
* everything is in the hand of the withdrawal group.
*/
PendingWithdrawing = 0x0100_0002,
-
- SuspendedMerge = 0x0110_0000,
- SuspendedMergeKycRequired = 0x0110_0001,
SuspendedWithdrawing = 0x0110_0002,
+ PendingBalanceKycRequired = 0x0100_0003,
+ SuspendedBalanceKycRequired = 0x0110_0003,
+
+ PendingBalanceKycInit = 0x0100_0004,
+ SuspendedBalanceKycInit = 0x0110_0004,
+
DialogProposed = 0x0101_0000,
Done = 0x0500_0000,
@@ -2017,6 +2126,9 @@ export interface PeerPushPaymentIncomingRecord {
*/
status: PeerPushCreditStatus;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
+
/**
* Associated withdrawal group.
*/
@@ -2030,9 +2142,9 @@ export interface PeerPushPaymentIncomingRecord {
*/
currency: string | undefined;
- kycInfo?: KycPendingInfo;
+ kycPaytoHash?: string;
- kycUrl?: string;
+ kycAccessToken?: string;
}
export enum PeerPullDebitRecordStatus {
@@ -2093,9 +2205,26 @@ export interface PeerPullPaymentIncomingRecord {
abortRefreshGroupId?: string;
+ abortReason?: TalerErrorDetail;
+ failReason?: TalerErrorDetail;
+
coinSel?: PeerPullPaymentCoinSelection;
}
+export enum ReserveRecordStatus {
+ // Need to call the "/kyc-wallet" endpoint
+ PendingLegiInit = 0x0100_0001,
+ SuspendedLegiInit = 0x0110_0001,
+ // Need to wait for user to pass legitimization
+ PendingLegi = 0x0100_0002,
+ SuspendedLegi = 0x0110_0002,
+
+ /**
+ * Done with KYC.
+ */
+ Done = 0x0500_0000,
+}
+
/**
* Store for extra information about a reserve.
*
@@ -2108,6 +2237,29 @@ export interface ReserveRecord {
rowId?: number;
reservePub: string;
reservePriv: string;
+
+ status?: ReserveRecordStatus;
+
+ requirementRow?: number;
+
+ /**
+ * Balance threshold that we're currently requesting KYC for.
+ */
+ thresholdRequested?: AmountString;
+
+ /**
+ * Balance threshold that we already have passed KYC for.
+ */
+ thresholdGranted?: AmountString;
+
+ /**
+ * Threshold that will trigger the next KYC.
+ */
+ thresholdNext?: AmountString;
+
+ kycAccessToken?: string;
+
+ amlReview?: boolean;
}
export interface OperationRetryRecord {
@@ -2300,13 +2452,26 @@ export interface GlobalCurrencyExchangeRecord {
}
/**
- * Primary key: transactionItem.transactionId
+ * Metadata for a transaction.
+ * This object store is effectively a materialzed view of transactions gathered
+ * from various other object stores.
+ *
+ * Primary key: transactionId
*/
-export interface TransactionRecord {
+export interface TransactionMetaRecord {
+ /**
+ * Transaction identifier.
+ * Also determines the type of the transaction.
+ */
+ transactionId: string;
+
+ timestamp: DbPreciseTimestamp;
+
/**
- * Transaction item returned to the client.
+ * Status of the transaction, matches the status enum of the
+ * transaction of the type determined by the transaction ID.
*/
- transactionItem: Transaction;
+ status: number;
/**
* Exchanges involved in the transaction.
@@ -2339,6 +2504,23 @@ export interface DenomLossEventRecord {
exchangeBaseUrl: string;
}
+export interface CurrencyInfoRecord {
+ /**
+ * Stringified scope info.
+ */
+ scopeInfoStr: string;
+
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+}
+
/**
* Schema definition for the IndexedDB
* wallet database.
@@ -2358,21 +2540,33 @@ export const WalletStoresV1 = {
}),
},
}),
- transactions: describeStoreV2({
- recordCodec: passthroughCodec<TransactionRecord>(),
- storeName: "transactions",
- keyPath: "transactionItem.transactionId",
- versionAdded: 7,
+ transactionsMeta: describeStoreV2({
+ recordCodec: passthroughCodec<TransactionMetaRecord>(),
+ storeName: "transactionsMeta",
+ keyPath: "transactionId",
+ versionAdded: 13,
indexes: {
byCurrency: describeIndex("byCurrency", "currency", {
- versionAdded: 7,
+ versionAdded: 13,
}),
byExchange: describeIndex("byExchange", "exchanges", {
- versionAdded: 7,
+ versionAdded: 13,
multiEntry: true,
}),
+ byTimestamp: describeIndex("byTimestamp", "timestamp", {
+ versionAdded: 13,
+ }),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 13,
+ }),
},
}),
+ currencyInfo: describeStoreV2({
+ recordCodec: passthroughCodec<CurrencyInfoRecord>(),
+ storeName: "currencyInfo",
+ keyPath: "scopeInfoStr",
+ versionAdded: 12,
+ }),
globalCurrencyAuditors: describeStoreV2({
recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(),
storeName: "globalCurrencyAuditors",
@@ -2423,6 +2617,12 @@ export const WalletStoresV1 = {
}),
},
),
+ coinHistory: describeStoreV2({
+ storeName: "coinHistory",
+ recordCodec: passthroughCodec<CoinHistoryRecord>(),
+ keyPath: "coinPub",
+ versionAdded: 11,
+ }),
coins: describeStore(
"coins",
describeContents<CoinRecord>({
@@ -2766,6 +2966,22 @@ export const WalletStoresV1 = {
}),
{},
),
+ // Obsolete store, not used anymore
+ _obsolete_transactions: describeStoreV2({
+ recordCodec: passthroughCodec<unknown>(),
+ storeName: "transactions",
+ keyPath: "transactionItem.transactionId",
+ versionAdded: 7,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 7,
+ }),
+ byExchange: describeIndex("byExchange", "exchanges", {
+ versionAdded: 7,
+ multiEntry: true,
+ }),
+ },
+ }),
};
export type WalletDbStoresArr = Array<StoreNames<typeof WalletStoresV1>>;
@@ -3157,6 +3373,7 @@ export function clearDatabase(db: IDBDatabase): Promise<void> {
for (let i = 0; i < db.objectStoreNames.length; i++) {
stores.push(db.objectStoreNames[i]);
}
+ logger.info(`clearing object stores: ${j2s(stores)}`);
const tx = db.transaction(stores, "readwrite");
for (const store of stores) {
tx.objectStore(store).clear();
@@ -3325,3 +3542,75 @@ export async function deleteTalerDatabase(
req.onsuccess = () => resolve();
});
}
+
+/**
+ * High-level helpers to access the database.
+ * Eventually all access to the database should
+ * go through helpers in this namespace.
+ */
+export namespace WalletDbHelpers {
+ export interface GetCurrencyInfoDbResult {
+ /**
+ * Currency specification.
+ */
+ currencySpec: CurrencySpecification;
+
+ /**
+ * How did the currency info get set?
+ */
+ source: "exchange" | "user" | "preset";
+ }
+
+ export interface StoreCurrencyInfoDbRequest {
+ scopeInfo: ScopeInfo;
+ currencySpec: CurrencySpecification;
+ source: "exchange" | "user" | "preset";
+ }
+
+ export async function getCurrencyInfo(
+ tx: WalletDbReadOnlyTransaction<["currencyInfo"]>,
+ scopeInfo: ScopeInfo,
+ ): Promise<GetCurrencyInfoDbResult | undefined> {
+ const s = stringifyScopeInfo(scopeInfo);
+ const res = await tx.currencyInfo.get(s);
+ if (!res) {
+ return undefined;
+ }
+ return {
+ currencySpec: res.currencySpec,
+ source: res.source,
+ };
+ }
+
+ /**
+ * Store currency info for a scope.
+ *
+ * Overrides existing currency infos.
+ */
+ export async function upsertCurrencyInfo(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+
+ export async function insertCurrencyInfoUnlessExists(
+ tx: WalletDbReadWriteTransaction<["currencyInfo"]>,
+ req: StoreCurrencyInfoDbRequest,
+ ): Promise<void> {
+ const scopeInfoStr = stringifyScopeInfo(req.scopeInfo);
+ const oldRec = await tx.currencyInfo.get(scopeInfoStr);
+ if (oldRec) {
+ return;
+ }
+ await tx.currencyInfo.put({
+ scopeInfoStr: stringifyScopeInfo(req.scopeInfo),
+ currencySpec: req.currencySpec,
+ source: req.source,
+ });
+ }
+}
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index ec9655e6f..bc0b6e428 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -56,9 +56,9 @@ import {
} from "@gnu-taler/taler-util/http";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { DenominationRecord } from "./db.js";
+import { isWithdrawableDenom } from "./denominations.js";
import { ExchangeInfo, downloadExchangeInfo } from "./exchanges.js";
import { assembleRefreshRevealRequest } from "./refresh.js";
-import { isWithdrawableDenom } from "./denominations.js";
import { getBankStatusUrl, getBankWithdrawalInfo } from "./withdraw.js";
export { downloadExchangeInfo };
@@ -85,26 +85,6 @@ export interface CoinInfo {
maxAge: number;
}
-/**
- * Check the status of a reserve, use long-polling to wait
- * until the reserve actually has been created.
- */
-export async function checkReserve(
- http: HttpRequestLibrary,
- exchangeBaseUrl: string,
- reservePub: string,
- longpollTimeoutMs: number = 500,
-): Promise<void> {
- const reqUrl = new URL(`reserves/${reservePub}`, exchangeBaseUrl);
- if (longpollTimeoutMs) {
- reqUrl.searchParams.set("timeout_ms", `${longpollTimeoutMs}`);
- }
- const resp = await http.fetch(reqUrl.href, { method: "GET" });
- if (resp.status !== 200) {
- throw new Error("reserve not okay");
- }
-}
-
export interface TopupReserveWithBankArgs {
http: HttpRequestLibrary;
reservePub: string;
@@ -415,3 +395,25 @@ export async function createTestingReserve(args: {
);
await readSuccessResponseJsonOrThrow(fbReq, codecForAny());
}
+
+/**
+ * Check the status of a reserve, use long-polling to wait
+ * until the reserve actually has been created.
+ */
+export async function checkReserve(
+ http: HttpRequestLibrary,
+ exchangeBaseUrl: string,
+ reservePub: string,
+ longpollTimeoutMs: number = 500,
+): Promise<void> {
+ const reqUrl = new URL(`reserves/${reservePub}`, exchangeBaseUrl);
+ if (longpollTimeoutMs) {
+ reqUrl.searchParams.set("timeout_ms", `${longpollTimeoutMs}`);
+ }
+ const resp = await http.fetch(reqUrl.href, {
+ method: "GET",
+ });
+ if (resp.status !== 200) {
+ throw new Error("reserve not okay");
+ }
+}
diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts
index ecc1fa881..9e62857bf 100644
--- a/packages/taler-wallet-core/src/denomSelection.ts
+++ b/packages/taler-wallet-core/src/denomSelection.ts
@@ -123,9 +123,6 @@ export function selectWithdrawalDenominations(
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- earliestDepositExpiration,
- ),
hasDenomWithAgeRestriction,
};
}
@@ -191,9 +188,6 @@ export function selectForcedWithdrawalDenominations(
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- earliestDepositExpiration,
- ),
hasDenomWithAgeRestriction,
};
}
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 2004c12cb..c1d67d2eb 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -27,27 +27,31 @@ import {
Amounts,
BatchDepositRequestCoin,
CancellationToken,
+ CheckDepositRequest,
+ CheckDepositResponse,
CoinRefreshRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DepositGroupFees,
+ DepositTransactionTrackingState,
Duration,
+ Exchange,
ExchangeBatchDepositRequest,
- ExchangeHandle,
ExchangeRefundRequest,
HttpStatusCode,
+ KycAuthTransferInfo,
Logger,
MerchantContractTerms,
NotificationType,
- PrepareDepositRequest,
- PrepareDepositResponse,
RefreshReason,
SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
+ TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TrackTransaction,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -55,12 +59,13 @@ import {
TransactionState,
TransactionType,
URL,
- WireFee,
assertUnreachable,
canonicalJson,
checkDbInvariant,
checkLogicInvariant,
+ codecForAccountKycStatus,
codecForBatchDepositSuccess,
+ codecForLegitimizationNeededResponse,
codecForTackTransactionAccepted,
codecForTackTransactionWired,
encodeCrock,
@@ -71,8 +76,13 @@ import {
parsePaytoUri,
stringToBytes,
} from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { selectPayCoins } from "./coinSelection.js";
+import {
+ readResponseJsonOrThrow,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
+} from "@gnu-taler/taler-util/http";
+import { selectPayCoins, selectPayCoinsInTx } from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -80,6 +90,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ runWithClientCancellation,
spendCoins,
} from "./common.js";
import {
@@ -88,12 +99,21 @@ import {
DepositInfoPerExchange,
DepositOperationStatus,
DepositTrackingInfo,
- KycPendingInfo,
RefreshOperationStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ timestampAbsoluteFromDb,
+ timestampPreciseFromDb,
timestampPreciseToDb,
+ timestampProtocolFromDb,
timestampProtocolToDb,
} from "./db.js";
-import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import {
+ getExchangeWireDetailsInTx,
+ getExchangeWireFee,
+ getScopeForAllExchanges,
+} from "./exchanges.js";
+import { EddsaKeyPairStrings } from "./index.js";
import {
extractContractData,
generateDepositPermissions,
@@ -106,10 +126,12 @@ import {
} from "./refresh.js";
import {
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+import { augmentPaytoUrisForKycTransfer } from "./withdraw.js";
/**
* Logger.
@@ -134,13 +156,156 @@ export class DepositTransactionContext implements TransactionContext {
});
}
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const dg = await tx.depositGroups.get(this.depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ const ort = await tx.operationRetries.get(this.taskId);
+
+ let deposited = true;
+ if (dg.statusPerCoin) {
+ for (const d of dg.statusPerCoin) {
+ if (d == DepositElementStatus.DepositPending) {
+ deposited = false;
+ }
+ }
+ } else {
+ deposited = false;
+ }
+
+ const trackingState: DepositTransactionTrackingState[] = [];
+
+ for (const ts of Object.values(dg.trackingState ?? {})) {
+ trackingState.push({
+ amountRaw: ts.amountRaw,
+ timestampExecuted: timestampProtocolFromDb(ts.timestampExecuted),
+ wireFee: ts.wireFee,
+ wireTransferId: ts.wireTransferId,
+ });
+ }
+
+ let wireTransferProgress = 0;
+ if (dg.statusPerCoin) {
+ wireTransferProgress =
+ (100 *
+ dg.statusPerCoin.reduce(
+ (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
+ 0,
+ )) /
+ dg.statusPerCoin.length;
+ }
+
+ let kycAuthTransferInfo: KycAuthTransferInfo | undefined = undefined;
+
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingDepositKycAuth:
+ case DepositOperationStatus.SuspendedDepositKycAuth: {
+ if (!dg.kycInfo) {
+ break;
+ }
+ const plainCreditPaytoUris: string[] = [];
+ const exchangeWire = await getExchangeWireDetailsInTx(
+ tx,
+ dg.kycInfo.exchangeBaseUrl,
+ );
+ if (exchangeWire) {
+ for (const acc of exchangeWire.wireInfo.accounts) {
+ if (acc.conversion_url) {
+ // Conversion accounts do not work for KYC auth!
+ continue;
+ }
+ plainCreditPaytoUris.push(acc.payto_uri);
+ }
+ }
+ kycAuthTransferInfo = {
+ debitPaytoUri: dg.wire.payto_uri,
+ accountPub: dg.merchantPub,
+ creditPaytoUris: augmentPaytoUrisForKycTransfer(
+ plainCreditPaytoUris,
+ dg.kycInfo?.paytoHash,
+ // FIXME: Query tiny amount from exchange.
+ `${dg.currency}:0.01`,
+ ),
+ };
+ break;
+ }
+ }
+
+ const txState = computeDepositTransactionStatus(dg);
+ return {
+ type: TransactionType.Deposit,
+ txState,
+ scopes: await getScopeForAllExchanges(
+ tx,
+ !dg.infoPerExchange ? [] : Object.keys(dg.infoPerExchange),
+ ),
+ txActions: computeDepositTransactionActions(dg),
+ amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost))
+ : Amounts.stringify(dg.totalPayCost),
+ timestamp: timestampPreciseFromDb(dg.timestampCreated),
+ targetPaytoUri: dg.wire.payto_uri,
+ wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId: dg.depositGroupId,
+ }),
+ wireTransferProgress,
+ depositGroupId: dg.depositGroupId,
+ trackingState,
+ deposited,
+ abortReason: dg.abortReason,
+ failReason: dg.failReason,
+ kycAuthTransferInfo,
+ kycPaytoHash: dg.kycInfo?.paytoHash,
+ kycAccessToken: dg.kycInfo?.accessToken,
+ kycUrl: dg.kycInfo
+ ? new URL(
+ `kyc-spa/${dg.kycInfo.accessToken}`,
+ dg.kycInfo.exchangeBaseUrl,
+ ).href
+ : undefined,
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+ }
+
+ /**
+ * Update the metadata of the transaction in the database.
+ */
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["depositGroups", "transactionsMeta"]>,
+ ): Promise<void> {
+ const depositRec = await tx.depositGroups.get(this.depositGroupId);
+ if (!depositRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: depositRec.operationStatus,
+ timestamp: depositRec.timestampCreated,
+ currency: depositRec.currency,
+ exchanges: Object.keys(depositRec.infoPerExchange ?? {}),
+ });
+ }
+
async deleteTransaction(): Promise<void> {
const depositGroupId = this.depositGroupId;
const ws = this.wex;
// FIXME: We should check first if we are in a final state
// where deletion is allowed.
await ws.db.runReadWriteTx(
- { storeNames: ["depositGroups", "tombstones"] },
+ { storeNames: ["depositGroups", "tombstones", "transactionsMeta"] },
async (tx) => {
const tipRecord = await tx.depositGroups.get(depositGroupId);
if (tipRecord) {
@@ -148,6 +313,7 @@ export class DepositTransactionContext implements TransactionContext {
await tx.tombstones.put({
id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
});
+ await this.updateTransactionMeta(tx);
}
},
);
@@ -157,7 +323,7 @@ export class DepositTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -169,11 +335,25 @@ export class DepositTransactionContext implements TransactionContext {
const oldState = computeDepositTransactionStatus(dg);
let newOpStatus: DepositOperationStatus | undefined;
switch (dg.operationStatus) {
+ case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FailedDeposit:
+ case DepositOperationStatus.FailedTrack:
+ case DepositOperationStatus.Finished:
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.SuspendedAggregateKyc:
+ case DepositOperationStatus.SuspendedDeposit:
+ case DepositOperationStatus.SuspendedDepositKyc:
+ case DepositOperationStatus.SuspendedTrack:
+ case DepositOperationStatus.SuspendedDepositKycAuth:
+ break;
+ case DepositOperationStatus.PendingDepositKyc:
+ newOpStatus = DepositOperationStatus.SuspendedDepositKyc;
+ break;
case DepositOperationStatus.PendingDeposit:
newOpStatus = DepositOperationStatus.SuspendedDeposit;
break;
- case DepositOperationStatus.PendingKyc:
- newOpStatus = DepositOperationStatus.SuspendedKyc;
+ case DepositOperationStatus.PendingAggregateKyc:
+ newOpStatus = DepositOperationStatus.SuspendedAggregateKyc;
break;
case DepositOperationStatus.PendingTrack:
newOpStatus = DepositOperationStatus.SuspendedTrack;
@@ -181,12 +361,18 @@ export class DepositTransactionContext implements TransactionContext {
case DepositOperationStatus.Aborting:
newOpStatus = DepositOperationStatus.SuspendedAborting;
break;
+ case DepositOperationStatus.PendingDepositKycAuth:
+ newOpStatus = DepositOperationStatus.SuspendedDepositKycAuth;
+ break;
+ default:
+ assertUnreachable(dg.operationStatus);
}
if (!newOpStatus) {
return undefined;
}
dg.operationStatus = newOpStatus;
await tx.depositGroups.put(dg);
+ await this.updateTransactionMeta(tx);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
@@ -197,10 +383,10 @@ export class DepositTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -211,17 +397,34 @@ export class DepositTransactionContext implements TransactionContext {
}
const oldState = computeDepositTransactionStatus(dg);
switch (dg.operationStatus) {
- case DepositOperationStatus.Finished:
- return undefined;
+ case DepositOperationStatus.PendingDepositKyc:
+ case DepositOperationStatus.SuspendedDepositKyc:
+ case DepositOperationStatus.PendingDepositKycAuth:
+ case DepositOperationStatus.SuspendedDepositKycAuth:
case DepositOperationStatus.PendingDeposit:
case DepositOperationStatus.SuspendedDeposit: {
dg.operationStatus = DepositOperationStatus.Aborting;
+ dg.abortReason = reason;
await tx.depositGroups.put(dg);
+ await this.updateTransactionMeta(tx);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
};
}
+ case DepositOperationStatus.PendingTrack:
+ case DepositOperationStatus.SuspendedTrack:
+ case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.Aborting:
+ case DepositOperationStatus.FailedDeposit:
+ case DepositOperationStatus.FailedTrack:
+ case DepositOperationStatus.Finished:
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.PendingAggregateKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
+ break;
+ default:
+ assertUnreachable(dg.operationStatus);
}
return undefined;
},
@@ -229,16 +432,12 @@ export class DepositTransactionContext implements TransactionContext {
wex.taskScheduler.stopShepherdTask(retryTag);
notifyTransition(wex, transactionId, transitionInfo);
wex.taskScheduler.startShepherdTask(retryTag);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
}
async resumeTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -250,24 +449,44 @@ export class DepositTransactionContext implements TransactionContext {
const oldState = computeDepositTransactionStatus(dg);
let newOpStatus: DepositOperationStatus | undefined;
switch (dg.operationStatus) {
+ case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.Aborting:
+ case DepositOperationStatus.FailedDeposit:
+ case DepositOperationStatus.FailedTrack:
+ case DepositOperationStatus.Finished:
+ case DepositOperationStatus.PendingAggregateKyc:
+ case DepositOperationStatus.PendingDeposit:
+ case DepositOperationStatus.PendingDepositKyc:
+ case DepositOperationStatus.PendingTrack:
+ case DepositOperationStatus.PendingDepositKycAuth:
+ break;
+ case DepositOperationStatus.SuspendedDepositKyc:
+ newOpStatus = DepositOperationStatus.PendingDepositKyc;
+ break;
case DepositOperationStatus.SuspendedDeposit:
newOpStatus = DepositOperationStatus.PendingDeposit;
break;
case DepositOperationStatus.SuspendedAborting:
newOpStatus = DepositOperationStatus.Aborting;
break;
- case DepositOperationStatus.SuspendedKyc:
- newOpStatus = DepositOperationStatus.PendingKyc;
+ case DepositOperationStatus.SuspendedAggregateKyc:
+ newOpStatus = DepositOperationStatus.PendingAggregateKyc;
break;
case DepositOperationStatus.SuspendedTrack:
newOpStatus = DepositOperationStatus.PendingTrack;
break;
+ case DepositOperationStatus.SuspendedDepositKycAuth:
+ newOpStatus = DepositOperationStatus.PendingDepositKycAuth;
+ break;
+ default:
+ assertUnreachable(dg.operationStatus);
}
if (!newOpStatus) {
return undefined;
}
dg.operationStatus = newOpStatus;
await tx.depositGroups.put(dg);
+ await this.updateTransactionMeta(tx);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
@@ -278,10 +497,10 @@ export class DepositTransactionContext implements TransactionContext {
wex.taskScheduler.startShepherdTask(retryTag);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, depositGroupId, transactionId, taskId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -291,26 +510,46 @@ export class DepositTransactionContext implements TransactionContext {
return undefined;
}
const oldState = computeDepositTransactionStatus(dg);
+ let newState: DepositOperationStatus;
switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingAggregateKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
case DepositOperationStatus.SuspendedAborting:
case DepositOperationStatus.Aborting: {
- dg.operationStatus = DepositOperationStatus.Failed;
- await tx.depositGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeDepositTransactionStatus(dg),
- };
+ newState = DepositOperationStatus.FailedDeposit;
+ break;
+ }
+ case DepositOperationStatus.PendingTrack:
+ case DepositOperationStatus.SuspendedTrack: {
+ newState = DepositOperationStatus.FailedTrack;
+ break;
}
+ case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FailedDeposit:
+ case DepositOperationStatus.FailedTrack:
+ case DepositOperationStatus.Finished:
+ case DepositOperationStatus.PendingDeposit:
+ case DepositOperationStatus.PendingDepositKyc:
+ case DepositOperationStatus.PendingDepositKycAuth:
+ case DepositOperationStatus.SuspendedDeposit:
+ case DepositOperationStatus.SuspendedDepositKyc:
+ case DepositOperationStatus.SuspendedDepositKycAuth:
+ throw Error("failing not supported in current state");
+ default:
+ assertUnreachable(dg.operationStatus);
}
- return undefined;
+ dg.operationStatus = newState;
+ dg.failReason = reason;
+ await tx.depositGroups.put(dg);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
},
);
wex.taskScheduler.stopShepherdTask(taskId);
notifyTransition(wex, transactionId, transitionInfo);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
}
}
@@ -331,7 +570,7 @@ export function computeDepositTransactionStatus(
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Deposit,
};
- case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.PendingAggregateKyc:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycRequired,
@@ -341,7 +580,7 @@ export function computeDepositTransactionStatus(
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Track,
};
- case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.KycRequired,
@@ -359,18 +598,48 @@ export function computeDepositTransactionStatus(
return {
major: TransactionMajorState.Aborting,
};
- case DepositOperationStatus.Aborted:
+ case DepositOperationStatus.AbortedDeposit:
return {
major: TransactionMajorState.Aborted,
};
- case DepositOperationStatus.Failed:
+ case DepositOperationStatus.FailedDeposit:
return {
major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.Deposit,
+ };
+ case DepositOperationStatus.FailedTrack:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.Track,
};
case DepositOperationStatus.SuspendedAborting:
return {
major: TransactionMajorState.SuspendedAborting,
};
+ case DepositOperationStatus.PendingDepositKyc:
+ return {
+ major: TransactionMajorState.Pending,
+ // We lie to the UI by hiding the specific KYC state.
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.SuspendedDepositKyc:
+ return {
+ major: TransactionMajorState.Suspended,
+ // We lie to the UI by hiding the specific KYC state.
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.PendingDepositKycAuth:
+ return {
+ major: TransactionMajorState.Pending,
+ // We lie to the UI by hiding the specific KYC state.
+ minor: TransactionMinorState.KycAuthRequired,
+ };
+ case DepositOperationStatus.SuspendedDepositKycAuth:
+ return {
+ major: TransactionMajorState.Suspended,
+ // We lie to the UI by hiding the specific KYC state.
+ minor: TransactionMinorState.KycAuthRequired,
+ };
default:
assertUnreachable(dg.operationStatus);
}
@@ -400,13 +669,14 @@ export function computeDepositTransactionActions(
TransactionAction.Fail,
TransactionAction.Suspend,
];
- case DepositOperationStatus.Aborted:
+ case DepositOperationStatus.AbortedDeposit:
return [TransactionAction.Delete];
- case DepositOperationStatus.Failed:
+ case DepositOperationStatus.FailedDeposit:
+ case DepositOperationStatus.FailedTrack:
return [TransactionAction.Delete];
case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail];
- case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.PendingAggregateKyc:
return [
TransactionAction.Retry,
TransactionAction.Suspend,
@@ -416,11 +686,19 @@ export function computeDepositTransactionActions(
return [
TransactionAction.Retry,
TransactionAction.Suspend,
- TransactionAction.Abort,
+ TransactionAction.Fail,
];
- case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc:
return [TransactionAction.Resume, TransactionAction.Fail];
case DepositOperationStatus.SuspendedTrack:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case DepositOperationStatus.PendingDepositKyc:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case DepositOperationStatus.SuspendedDepositKyc:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case DepositOperationStatus.PendingDepositKycAuth:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case DepositOperationStatus.SuspendedDepositKycAuth:
return [TransactionAction.Resume, TransactionAction.Abort];
default:
assertUnreachable(dg.operationStatus);
@@ -515,15 +793,19 @@ async function refundDepositGroup(
const currency = Amounts.currencyOf(depositGroup.totalPayCost);
+ const ctx = new DepositTransactionContext(wex, depositGroup.depositGroupId);
+
const res = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"depositGroups",
"refreshGroups",
"refreshSessions",
- "coins",
- "denominations",
- "coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -555,6 +837,7 @@ async function refundDepositGroup(
newDg.abortRefreshGroupId = refreshRes.refreshGroupId;
}
await tx.depositGroups.put(newDg);
+ await ctx.updateTransactionMeta(tx);
return { refreshRes };
},
);
@@ -587,12 +870,9 @@ async function waitForRefreshOnDepositGroup(
): Promise<TaskRunResult> {
const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId: depositGroup.depositGroupId,
- });
+ const ctx = new DepositTransactionContext(wex, depositGroup.depositGroupId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups", "refreshGroups"] },
+ { storeNames: ["depositGroups", "refreshGroups", "transactionsMeta"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: DepositOperationStatus | undefined;
@@ -600,14 +880,14 @@ async function waitForRefreshOnDepositGroup(
// Maybe it got manually deleted? Means that we should
// just go into aborted.
logger.warn("no aborting refresh group found for deposit group");
- newOpState = DepositOperationStatus.Aborted;
+ newOpState = DepositOperationStatus.AbortedDeposit;
} else {
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
- newOpState = DepositOperationStatus.Aborted;
+ newOpState = DepositOperationStatus.AbortedDeposit;
} else if (
refreshGroup.operationStatus === RefreshOperationStatus.Failed
) {
- newOpState = DepositOperationStatus.Aborted;
+ newOpState = DepositOperationStatus.AbortedDeposit;
}
}
if (newOpState) {
@@ -619,17 +899,14 @@ async function waitForRefreshOnDepositGroup(
newDg.operationStatus = newOpState;
const newTxState = computeDepositTransactionStatus(newDg);
await tx.depositGroups.put(newDg);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
}
return undefined;
},
);
- notifyTransition(wex, transactionId, transitionInfo);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
@@ -647,65 +924,234 @@ async function processDepositGroupAborting(
return waitForRefreshOnDepositGroup(wex, depositGroup);
}
+/**
+ * Process the transaction in states where KYC is required.
+ * Used for both the deposit KYC and aggregate KYC.
+ */
async function processDepositGroupPendingKyc(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
const { depositGroupId } = depositGroup;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
const kycInfo = depositGroup.kycInfo;
- const userType = "individual";
if (!kycInfo) {
throw Error("invalid DB state, in pending(kyc), but no kycInfo present");
}
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: depositGroup.merchantPriv,
+ accountPub: depositGroup.merchantPub,
+ });
+
const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ `kyc-check/${kycInfo.paytoHash}`,
kycInfo.exchangeBaseUrl,
);
- url.searchParams.set("timeout_ms", "10000");
- logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: wex.cancellationToken,
- });
+
+ const kycStatusRes = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`kyc url ${url.href}`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ });
+ },
+ );
+
+ logger.trace(
+ `request to ${kycStatusRes.requestUrl} returned status ${kycStatusRes.status}`,
+ );
+
if (
kycStatusRes.status === HttpStatusCode.Ok ||
- //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const newDg = await tx.depositGroups.get(depositGroupId);
if (!newDg) {
return;
}
- if (newDg.operationStatus !== DepositOperationStatus.PendingKyc) {
- return;
- }
const oldTxState = computeDepositTransactionStatus(newDg);
- newDg.operationStatus = DepositOperationStatus.PendingTrack;
- const newTxState = computeDepositTransactionStatus(newDg);
+ switch (newDg.operationStatus) {
+ case DepositOperationStatus.PendingAggregateKyc:
+ newDg.operationStatus = DepositOperationStatus.PendingTrack;
+ break;
+ case DepositOperationStatus.PendingDepositKyc:
+ newDg.operationStatus = DepositOperationStatus.PendingDeposit;
+ break;
+ default:
+ return;
+ }
await tx.depositGroups.put(newDg);
+ await ctx.updateTransactionMeta(tx);
+ const newTxState = computeDepositTransactionStatus(newDg);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- // FIXME: Do we have to update the URL here?
+ const statusResp = await readResponseJsonOrThrow(
+ kycStatusRes,
+ codecForAccountKycStatus(),
+ );
+ logger.info(`kyc still pending (HTTP 202): ${j2s(statusResp)}`);
} else {
- throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ throwUnexpectedRequestError(
+ kycStatusRes,
+ await readTalerErrorResponse(kycStatusRes),
+ );
}
return TaskRunResult.backoff();
}
+async function processDepositGroupPendingKycAuth(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+): Promise<TaskRunResult> {
+ const { depositGroupId } = depositGroup;
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
+
+ const kycInfo = depositGroup.kycInfo;
+
+ if (!kycInfo) {
+ throw Error(
+ "invalid DB state, in pending(kyc-auth), but no kycInfo present",
+ );
+ }
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: depositGroup.merchantPriv,
+ accountPub: depositGroup.merchantPub,
+ });
+
+ const url = new URL(
+ `kyc-check/${kycInfo.paytoHash}`,
+ kycInfo.exchangeBaseUrl,
+ );
+
+ // lpt=1 => wait for the KYC auth transfer (access token available)
+ url.searchParams.set("lpt", "1");
+
+ const kycStatusRes = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`kyc url ${url.href}`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ });
+ },
+ );
+
+ logger.info(`merchant pub: ${depositGroup.merchantPub}`);
+
+ logger.info(
+ `kyc-check for auth longpoll result status: ${kycStatusRes.status}`,
+ );
+
+ switch (kycStatusRes.status) {
+ case HttpStatusCode.Ok:
+ return await transitionKycAuthSuccess(ctx);
+ case HttpStatusCode.NoContent:
+ return await transitionKycAuthSuccess(ctx);
+ case HttpStatusCode.Accepted:
+ return await transitionKycAuthSuccess(ctx);
+ case HttpStatusCode.Conflict:
+ // FIXME: Consider also checking error code
+ logger.info("kyc still pending");
+ return TaskRunResult.longpollReturnedPending();
+ default:
+ throwUnexpectedRequestError(
+ kycStatusRes,
+ await readTalerErrorResponse(kycStatusRes),
+ );
+ }
+}
+
+async function transitionKycAuthSuccess(
+ ctx: DepositTransactionContext,
+): Promise<TaskRunResult> {
+ const transitionInfo = await ctx.wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "transactionsMeta"] },
+ async (tx) => {
+ const newDg = await tx.depositGroups.get(ctx.depositGroupId);
+ if (!newDg) {
+ return;
+ }
+ const oldTxState = computeDepositTransactionStatus(newDg);
+ switch (newDg.operationStatus) {
+ case DepositOperationStatus.PendingDepositKycAuth:
+ newDg.operationStatus = DepositOperationStatus.PendingDeposit;
+ break;
+ default:
+ return;
+ }
+ await tx.depositGroups.put(newDg);
+ await ctx.updateTransactionMeta(tx);
+ const newTxState = computeDepositTransactionStatus(newDg);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(ctx.wex, ctx.transactionId, transitionInfo);
+ if (transitionInfo) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+}
+
+/**
+ * Finds the reserve key pair of the most recent withdrawal
+ * with the given exchange.
+ * Returns undefined if no such withdrawal exists.
+ */
+async function getLastWithdrawalKeyPair(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<EddsaKeyPairStrings | undefined> {
+ let candidateTimestamp: AbsoluteTime | undefined = undefined;
+ let candidateRes: EddsaKeyPairStrings | undefined = undefined;
+ await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
+ const withdrawalRecs =
+ await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ for (const rec of withdrawalRecs) {
+ if (!rec.timestampFinish) {
+ continue;
+ }
+ const currTimestamp = timestampAbsoluteFromDb(rec.timestampFinish);
+ if (
+ candidateTimestamp == null ||
+ AbsoluteTime.cmp(currTimestamp, candidateTimestamp) > 0
+ ) {
+ candidateTimestamp = currTimestamp;
+ candidateRes = {
+ priv: rec.reservePriv,
+ pub: rec.reservePub,
+ };
+ }
+ }
+ });
+ return candidateRes;
+}
+
/**
* Tracking information from the exchange indicated that
* KYC is required. We need to check the KYC info
@@ -714,60 +1160,116 @@ async function processDepositGroupPendingKyc(
async function transitionToKycRequired(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
- kycInfo: KycPendingInfo,
+ kycPaytoHash: string,
exchangeUrl: string,
): Promise<TaskRunResult> {
const { depositGroupId } = depositGroup;
- const userType = "individual";
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: depositGroup.merchantPriv,
+ accountPub: depositGroup.merchantPub,
});
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
+ const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl);
logger.info(`kyc url ${url.href}`);
- const kycStatusReq = await wex.http.fetch(url.href, {
+ const kycStatusResp = await wex.http.fetch(url.href, {
method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
});
- if (kycStatusReq.status === HttpStatusCode.Ok) {
+ logger.trace(`response status of initial kyc-check: ${kycStatusResp.status}`);
+ if (kycStatusResp.status === HttpStatusCode.Ok) {
logger.warn("kyc requested, but already fulfilled");
return TaskRunResult.backoff();
- } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusReq.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
+ } else if (kycStatusResp.status === HttpStatusCode.Accepted) {
+ const statusResp = await readResponseJsonOrThrow(
+ kycStatusResp,
+ codecForAccountKycStatus(),
+ );
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return undefined;
}
- if (dg.operationStatus !== DepositOperationStatus.PendingTrack) {
- return undefined;
- }
const oldTxState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingTrack:
+ dg.operationStatus = DepositOperationStatus.PendingAggregateKyc;
+ break;
+ case DepositOperationStatus.PendingDeposit:
+ dg.operationStatus = DepositOperationStatus.PendingDepositKyc;
+ break;
+ default:
+ return;
+ }
dg.kycInfo = {
exchangeBaseUrl: exchangeUrl,
- kycUrl: kycStatus.kyc_url,
- paytoHash: kycInfo.paytoHash,
- requirementRow: kycInfo.requirementRow,
+ paytoHash: kycPaytoHash,
+ accessToken: statusResp.access_token,
};
await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computeDepositTransactionStatus(dg);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
- return TaskRunResult.finished();
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ return TaskRunResult.progress();
} else {
- throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
+ throwUnexpectedRequestError(
+ kycStatusResp,
+ await readTalerErrorResponse(kycStatusResp),
+ );
}
}
+async function transitionToKycAuthRequired(
+ wex: WalletExecutionContext,
+ depositGroup: DepositGroupRecord,
+ kycPaytoHash: string,
+ exchangeUrl: string,
+): Promise<TaskRunResult> {
+ const { depositGroupId } = depositGroup;
+
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "transactionsMeta"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.PendingTrack:
+ throw Error("not yet supported");
+ break;
+ case DepositOperationStatus.PendingDeposit:
+ dg.operationStatus = DepositOperationStatus.PendingDepositKycAuth;
+ break;
+ default:
+ return;
+ }
+ dg.kycInfo = {
+ exchangeBaseUrl: exchangeUrl,
+ paytoHash: kycPaytoHash,
+ };
+ await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ },
+ );
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ return TaskRunResult.progress();
+}
+
async function processDepositGroupPendingTrack(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
@@ -784,7 +1286,9 @@ async function processDepositGroupPendingTrack(
"unable to refund deposit group without coin selection (selection missing)",
);
}
+ logger.trace(`tracking deposit group, status ${j2s(statusPerCoin)}`);
const { depositGroupId } = depositGroup;
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
for (let i = 0; i < statusPerCoin.length; i++) {
const coinPub = payCoinSelection.coinPubs[i];
// FIXME: Make the URL part of the coin selection?
@@ -813,20 +1317,16 @@ async function processDepositGroupPendingTrack(
exchangeBaseUrl,
);
+ logger.trace(`track response: ${j2s(track)}`);
if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) {
const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
- const { requirement_row: requirementRow } = track;
- const kycInfo: KycPendingInfo = {
- paytoHash,
- requirementRow,
- };
return transitionToKycRequired(
wex,
depositGroup,
- kycInfo,
+ paytoHash,
exchangeBaseUrl,
);
} else {
@@ -866,7 +1366,7 @@ async function processDepositGroupPendingTrack(
if (updatedTxStatus !== undefined) {
await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -894,6 +1394,7 @@ async function processDepositGroupPendingTrack(
dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
}
await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
},
);
}
@@ -902,7 +1403,7 @@ async function processDepositGroupPendingTrack(
let allWired = true;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -924,21 +1425,14 @@ async function processDepositGroupPendingTrack(
);
dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
}
const newTxState = computeDepositTransactionStatus(dg);
return { oldTxState, newTxState };
},
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
if (allWired) {
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
return TaskRunResult.finished();
} else {
return TaskRunResult.longpollReturnedPending();
@@ -969,56 +1463,28 @@ async function processDepositGroupPendingDeposit(
"",
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId,
- });
+ const ctx = new DepositTransactionContext(wex, depositGroupId);
// Check for cancellation before expensive operations.
cancellationToken?.throwIfCancelled();
if (!depositGroup.payCoinSelection) {
logger.info("missing coin selection for deposit group, selecting now");
- // FIXME: Consider doing the coin selection inside the txn
- const payCoinSel = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins: [],
- });
-
- switch (payCoinSel.type) {
- case "success":
- logger.info("coin selection success");
- break;
- case "failure":
- logger.info("coin selection failure");
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
- },
- );
- case "prospective":
- logger.info("coin selection prospective");
- throw Error("insufficient balance (waiting on pending refresh)");
- default:
- assertUnreachable(payCoinSel);
- }
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
+ "contractTerms",
+ "exchanges",
+ "exchangeDetails",
"depositGroups",
"coins",
"coinAvailability",
+ "coinHistory",
"refreshGroups",
"refreshSessions",
"denominations",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1029,6 +1495,46 @@ async function processDepositGroupPendingDeposit(
if (dg.statusPerCoin) {
return false;
}
+
+ const contractTermsRec = tx.contractTerms.get(
+ depositGroup.contractTermsHash,
+ );
+ if (!contractTermsRec) {
+ throw Error("contract terms for deposit not found in database");
+ }
+
+ const payCoinSel = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ depositPaytoUri: dg.wire.payto_uri,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ switch (payCoinSel.type) {
+ case "success":
+ logger.info("coin selection success");
+ break;
+ case "failure":
+ logger.info("coin selection failure");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails:
+ payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ logger.info("coin selection prospective");
+ throw Error("insufficient balance (waiting on pending refresh)");
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
dg.payCoinSelection = {
coinContributions: payCoinSel.coinSel.coins.map(
(x) => x.contribution,
@@ -1040,8 +1546,9 @@ async function processDepositGroupPendingDeposit(
() => DepositElementStatus.DepositPending,
);
await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
await spendCoins(wex, tx, {
- allocationId: transactionId,
+ transactionId: ctx.transactionId,
coinPubs: dg.payCoinSelection.coinPubs,
contributions: dg.payCoinSelection.coinContributions.map((x) =>
Amounts.parseOrThrow(x),
@@ -1074,7 +1581,7 @@ async function processDepositGroupPendingDeposit(
}
// We need to do one batch per exchange.
- for (const exchangeUrl of exchanges.values()) {
+ for (const exchangeBaseUrl of exchanges.values()) {
const coins: BatchDepositRequestCoin[] = [];
const batchIndexes: number[] = [];
@@ -1091,7 +1598,7 @@ async function processDepositGroupPendingDeposit(
for (let i = 0; i < depositPermissions.length; i++) {
const perm = depositPermissions[i];
- if (perm.exchange_url != exchangeUrl) {
+ if (perm.exchange_url != exchangeBaseUrl) {
continue;
}
coins.push({
@@ -1107,7 +1614,7 @@ async function processDepositGroupPendingDeposit(
// Check for cancellation before making network request.
cancellationToken?.throwIfCancelled();
- const url = new URL(`batch-deposit`, exchangeUrl);
+ const url = new URL(`batch-deposit`, exchangeBaseUrl);
logger.info(`depositing to ${url.href}`);
logger.trace(`deposit request: ${j2s(batchReq)}`);
const httpResp = await wex.http.fetch(url.href, {
@@ -1115,13 +1622,44 @@ async function processDepositGroupPendingDeposit(
body: batchReq,
cancellationToken: cancellationToken,
});
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Accepted:
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ const kycLegiNeededResp = await readResponseJsonOrThrow(
+ httpResp,
+ codecForLegitimizationNeededResponse(),
+ );
+ logger.info(
+ `kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`,
+ );
+ if (kycLegiNeededResp.bad_kyc_auth) {
+ return transitionToKycAuthRequired(
+ wex,
+ depositGroup,
+ kycLegiNeededResp.h_payto,
+ exchangeBaseUrl,
+ );
+ } else {
+ return transitionToKycRequired(
+ wex,
+ depositGroup,
+ kycLegiNeededResp.h_payto,
+ exchangeBaseUrl,
+ );
+ }
+ }
+ }
+
await readSuccessResponseJsonOrThrow(
httpResp,
codecForBatchDepositSuccess(),
);
await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -1138,12 +1676,13 @@ async function processDepositGroupPendingDeposit(
await tx.depositGroups.put(dg);
}
}
+ await ctx.updateTransactionMeta(tx);
},
);
}
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups"] },
+ { storeNames: ["depositGroups", "transactionsMeta"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -1152,12 +1691,13 @@ async function processDepositGroupPendingDeposit(
const oldTxState = computeDepositTransactionStatus(dg);
dg.operationStatus = DepositOperationStatus.PendingTrack;
await tx.depositGroups.put(dg);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computeDepositTransactionStatus(dg);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
}
@@ -1168,6 +1708,10 @@ export async function processDepositGroup(
wex: WalletExecutionContext,
depositGroupId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
const depositGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["depositGroups"] },
async (tx) => {
@@ -1182,65 +1726,20 @@ export async function processDepositGroup(
switch (depositGroup.operationStatus) {
case DepositOperationStatus.PendingTrack:
return processDepositGroupPendingTrack(wex, depositGroup);
- case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.PendingAggregateKyc:
+ case DepositOperationStatus.PendingDepositKyc:
return processDepositGroupPendingKyc(wex, depositGroup);
case DepositOperationStatus.PendingDeposit:
return processDepositGroupPendingDeposit(wex, depositGroup);
case DepositOperationStatus.Aborting:
return processDepositGroupAborting(wex, depositGroup);
+ case DepositOperationStatus.PendingDepositKycAuth:
+ return processDepositGroupPendingKycAuth(wex, depositGroup);
}
return TaskRunResult.finished();
}
-/**
- * FIXME: Consider moving this to exchanges.ts.
- */
-async function getExchangeWireFee(
- wex: WalletExecutionContext,
- wireType: string,
- baseUrl: string,
- time: TalerProtocolTimestamp,
-): Promise<WireFee> {
- const exchangeDetails = await wex.db.runReadOnlyTx(
- { storeNames: ["exchangeDetails", "exchanges"] },
- async (tx) => {
- const ex = await tx.exchanges.get(baseUrl);
- if (!ex || !ex.detailsPointer) return undefined;
- return await tx.exchangeDetails.indexes.byPointer.get([
- baseUrl,
- ex.detailsPointer.currency,
- ex.detailsPointer.masterPublicKey,
- ]);
- },
- );
-
- if (!exchangeDetails) {
- throw Error(`exchange missing: ${baseUrl}`);
- }
-
- const fees = exchangeDetails.wireInfo.feesForType[wireType];
- if (!fees || fees.length === 0) {
- throw Error(
- `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`,
- );
- }
- const fee = fees.find((x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.fromProtocolTimestamp(time),
- AbsoluteTime.fromProtocolTimestamp(x.startStamp),
- AbsoluteTime.fromProtocolTimestamp(x.endStamp),
- );
- });
- if (!fee) {
- throw Error(
- `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`,
- );
- }
-
- return fee;
-}
-
async function trackDeposit(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
@@ -1264,11 +1763,19 @@ async function trackDeposit(
wireHash,
});
url.searchParams.set("merchant_sig", sigResp.sig);
- url.searchParams.set("timeout_ms", "30000");
- const httpResp = await wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: wex.cancellationToken,
- });
+ const httpResp = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ // wait for the a 202 state where kyc_ok is false or a 200 OK response
+ url.searchParams.set("lpt", `1`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
logger.trace(`deposits response status: ${httpResp.status}`);
switch (httpResp.status) {
case HttpStatusCode.Accepted: {
@@ -1276,6 +1783,7 @@ async function trackDeposit(
httpResp,
codecForTackTransactionAccepted(),
);
+ logger.trace(`deposits response: ${j2s(accepted)}`);
return { type: "accepted", ...accepted };
}
case HttpStatusCode.Ok: {
@@ -1286,8 +1794,9 @@ async function trackDeposit(
return { type: "wired", ...wired };
}
default: {
- throw Error(
- `unexpected response from track-transaction (${httpResp.status})`,
+ throwUnexpectedRequestError(
+ httpResp,
+ await readTalerErrorResponse(httpResp),
);
}
}
@@ -1299,8 +1808,24 @@ async function trackDeposit(
*/
export async function checkDepositGroup(
wex: WalletExecutionContext,
- req: PrepareDepositRequest,
-): Promise<PrepareDepositResponse> {
+ req: CheckDepositRequest,
+): Promise<CheckDepositResponse> {
+ return await runWithClientCancellation(
+ wex,
+ "checkDepositGroup",
+ req.clientCancellationId,
+ () => internalCheckDepositGroup(wex, req),
+ );
+}
+
+/**
+ * Check if creating a deposit group is possible and calculate
+ * the associated fees.
+ */
+export async function internalCheckDepositGroup(
+ wex: WalletExecutionContext,
+ req: CheckDepositRequest,
+): Promise<CheckDepositResponse> {
const p = parsePaytoUri(req.depositPaytoUri);
if (!p) {
throw Error("invalid payto URI");
@@ -1308,7 +1833,7 @@ export async function checkDepositGroup(
const amount = Amounts.parseOrThrow(req.amount);
const currency = Amounts.currencyOf(amount);
- const exchangeInfos: ExchangeHandle[] = [];
+ const exchangeInfos: Exchange[] = [];
await wex.db.runReadOnlyTx(
{ storeNames: ["exchangeDetails", "exchanges"] },
@@ -1321,6 +1846,7 @@ export async function checkDepositGroup(
}
exchangeInfos.push({
master_pub: details.masterPublicKey,
+ priority: 1,
url: e.baseUrl,
});
}
@@ -1367,6 +1893,7 @@ export async function checkDepositGroup(
exchanges: contractData.allowedExchanges,
},
restrictWireMethod: contractData.wireMethod,
+ depositPaytoUri: req.depositPaytoUri,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
prevPayCoins: [],
@@ -1426,8 +1953,8 @@ export async function createDepositGroup(
wex: WalletExecutionContext,
req: CreateDepositGroupRequest,
): Promise<CreateDepositGroupResponse> {
- const p = parsePaytoUri(req.depositPaytoUri);
- if (!p) {
+ const depositPayto = parsePaytoUri(req.depositPaytoUri);
+ if (!depositPayto) {
throw Error("invalid payto URI");
}
@@ -1458,15 +1985,73 @@ export async function createDepositGroup(
AbsoluteTime.addDuration(now, Duration.fromSpec({ minutes: 5 })),
);
const nowRounded = AbsoluteTime.toProtocolTimestamp(now);
+
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: exchangeInfos.map((x) => ({
+ exchangeBaseUrl: x.url,
+ exchangePub: x.master_pub,
+ })),
+ },
+ restrictWireMethod: depositPayto.targetType,
+ depositPaytoUri: req.depositPaytoUri,
+ contractTermsAmount: amount,
+ depositFeeLimit: amount,
+ prevPayCoins: [],
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (payCoinSel.type) {
+ case "success":
+ coins = payCoinSel.coinSel.coins;
+ break;
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = payCoinSel.result.prospectiveCoins;
+ break;
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ // Heuristic for the merchant key pair: If there's an exchange where we made
+ // a withdrawal from, use that key pair, so the user doesn't have to do
+ // a KYC transfer to establish a kyc account key pair.
+ // FIXME: Extend the heuristic to use the last used merchant key pair?
+ let merchantPair: EddsaKeyPairStrings | undefined = undefined;
+ if (coins.length > 0) {
+ const res = await getLastWithdrawalKeyPair(wex, coins[0].exchangeBaseUrl);
+ if (res) {
+ logger.info(
+ `reusing reserve pub ${res.pub} from last withdrawal to ${coins[0].exchangeBaseUrl}`,
+ );
+ merchantPair = res;
+ }
+ }
+ if (!merchantPair) {
+ logger.info(`creating new merchant key pair for deposit`);
+ merchantPair = await wex.cryptoApi.createEddsaKeypair({});
+ }
+
const noncePair = await wex.cryptoApi.createEddsaKeypair({});
- const merchantPair = await wex.cryptoApi.createEddsaKeypair({});
const wireSalt = encodeCrock(getRandomBytes(16));
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
const contractTerms: MerchantContractTerms = {
- exchanges: exchangeInfos,
+ exchanges: exchangeInfos.map((x) => ({
+ master_pub: x.master_pub,
+ priority: 1,
+ url: x.url,
+ })),
amount: req.amount,
max_fee: Amounts.stringify(amount),
- wire_method: p.targetType,
+ wire_method: depositPayto.targetType,
timestamp: nowRounded,
merchant_base_url: "",
summary: "",
@@ -1494,37 +2079,6 @@ export async function createDepositGroup(
"",
);
- const payCoinSel = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins: [],
- });
-
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
-
- switch (payCoinSel.type) {
- case "success":
- coins = payCoinSel.coinSel.coins;
- break;
- case "failure":
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
- },
- );
- case "prospective":
- coins = payCoinSel.result.prospectiveCoins;
- break;
- default:
- assertUnreachable(payCoinSel);
- }
-
const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
let depositGroupId: string;
@@ -1556,7 +2110,11 @@ export async function createDepositGroup(
}
const counterpartyEffectiveDepositAmount =
- await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins);
+ await getCounterpartyEffectiveDepositAmount(
+ wex,
+ depositPayto.targetType,
+ coins,
+ );
const depositGroup: DepositGroupRecord = {
contractTermsHash,
@@ -1606,20 +2164,22 @@ export async function createDepositGroup(
const newTxState = await wex.db.runReadWriteTx(
{
storeNames: [
- "depositGroups",
+ "coinAvailability",
+ "coinHistory",
"coins",
- "recoupGroups",
+ "contractTerms",
"denominations",
+ "depositGroups",
+ "recoupGroups",
"refreshGroups",
"refreshSessions",
- "coinAvailability",
- "contractTerms",
+ "transactionsMeta",
],
},
async (tx) => {
if (depositGroup.payCoinSelection) {
await spendCoins(wex, tx, {
- allocationId: transactionId,
+ transactionId,
coinPubs: depositGroup.payCoinSelection.coinPubs,
contributions: depositGroup.payCoinSelection.coinContributions.map(
(x) => Amounts.parseOrThrow(x),
@@ -1632,24 +2192,18 @@ export async function createDepositGroup(
contractTermsRaw: contractTerms,
h: contractTermsHash,
});
+ await ctx.updateTransactionMeta(tx);
return computeDepositTransactionStatus(depositGroup);
},
);
- wex.ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId,
+ notifyTransition(wex, transactionId, {
oldTxState: {
major: TransactionMajorState.None,
},
newTxState,
});
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
-
wex.taskScheduler.startShepherdTask(ctx.taskId);
return {
@@ -1662,7 +2216,7 @@ export async function createDepositGroup(
* Get the amount that will be deposited on the users bank
* account after depositing, not considering aggregation.
*/
-export async function getCounterpartyEffectiveDepositAmount(
+async function getCounterpartyEffectiveDepositAmount(
wex: WalletExecutionContext,
wireType: string,
pcs: SelectedProspectiveCoin[],
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
index 5cb9400be..65837f207 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -47,6 +47,8 @@ import {
RefreshOperationStatus,
timestampPreciseToDb,
} from "./db.js";
+import { DenomLossTransactionContext } from "./exchanges.js";
+import { RefreshTransactionContext } from "./refresh.js";
import { WalletExecutionContext } from "./wallet.js";
const logger = new Logger("dev-experiments.ts");
@@ -80,7 +82,7 @@ export async function applyDevExperiment(
case "insert-pending-refresh": {
const refreshGroupId = encodeCrock(getRandomBytes(32));
await wex.db.runReadWriteTx(
- { storeNames: ["refreshGroups"] },
+ { storeNames: ["refreshGroups", "transactionsMeta"] },
async (tx) => {
const newRg: RefreshGroupRecord = {
currency: "TESTKUDOS",
@@ -97,6 +99,8 @@ export async function applyDevExperiment(
infoPerExchange: {},
};
await tx.refreshGroups.put(newRg);
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+ await ctx.updateTransactionMeta(tx);
},
);
wex.taskScheduler.startShepherdTask(
@@ -109,7 +113,7 @@ export async function applyDevExperiment(
}
case "insert-denom-loss": {
await wex.db.runReadWriteTx(
- { storeNames: ["denomLossEvents"] },
+ { storeNames: ["denomLossEvents", "transactionsMeta"] },
async (tx) => {
const eventId = encodeCrock(getRandomBytes(32));
const newRg: DenomLossEventRecord = {
@@ -126,6 +130,11 @@ export async function applyDevExperiment(
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
await tx.denomLossEvents.put(newRg);
+ const ctx = new DenomLossTransactionContext(
+ wex,
+ newRg.denomLossEventId,
+ );
+ await ctx.updateTransactionMeta(tx);
},
);
return;
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index adb696de0..773ad0d59 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -25,13 +25,17 @@
*/
import {
AbsoluteTime,
+ AccountKycStatus,
+ AccountLimit,
AgeRestriction,
Amount,
+ AmountLike,
+ AmountString,
Amounts,
- AsyncFlag,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DeleteExchangeRequest,
DenomKeyType,
DenomLossEventType,
@@ -40,12 +44,15 @@ import {
DenominationPubKey,
Duration,
EddsaPublicKeyString,
+ EmptyObject,
ExchangeAuditor,
ExchangeDetailedResponse,
ExchangeGlobalFees,
ExchangeListItem,
ExchangeSignKeyJson,
ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ ExchangeWalletKycStatus,
ExchangeWireAccount,
ExchangesListResponse,
FeeDescription,
@@ -53,6 +60,8 @@ import {
GetExchangeResourcesResponse,
GetExchangeTosResult,
GlobalFees,
+ HttpStatusCode,
+ LegitimizationNeededResponse,
LibtoolVersion,
Logger,
NotificationType,
@@ -61,38 +70,54 @@ import {
RefreshReason,
ScopeInfo,
ScopeType,
+ StartExchangeWalletKycRequest,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ TestingWaitExchangeStateRequest,
+ TestingWaitWalletKycRequest,
+ Transaction,
+ TransactionAction,
TransactionIdStr,
TransactionMajorState,
TransactionState,
TransactionType,
URL,
+ WalletKycRequest,
WalletNotification,
WireFee,
WireFeeMap,
WireFeesJson,
WireInfo,
+ ZeroLimitedOperation,
assertUnreachable,
checkDbInvariant,
+ checkLogicInvariant,
+ codecForAccountKycStatus,
codecForExchangeKeysJson,
+ codecForLegitimizationNeededResponse,
durationMul,
encodeCrock,
getRandomBytes,
hashDenomPub,
+ hashPaytoUri,
j2s,
makeErrorDetail,
+ makeTalerErrorDetail,
parsePaytoUri,
+ stringifyReservePaytoUri,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
getExpiry,
+ readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
@@ -103,6 +128,7 @@ import {
TransactionContext,
computeDbBackoff,
constructTaskIdentifier,
+ genericWaitForState,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
getExchangeState,
@@ -118,6 +144,11 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
+ ReserveRecord,
+ ReserveRecordStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbAllStoresReadWriteTransaction,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletStoresV1,
@@ -140,6 +171,7 @@ import { createRefreshGroup } from "./refresh.js";
import {
constructTransactionIdentifier,
notifyTransition,
+ rematerializeTransactions,
} from "./transactions.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
@@ -187,7 +219,7 @@ async function downloadExchangeWithTermsOfService(
cancellationToken: wex.cancellationToken,
});
const tosText = await readSuccessResponseTextOrThrow(resp);
- const tosEtag = resp.headers.get("etag") || "unknown";
+ const tosEtag = resp.headers.get("taler-terms-version") || "unknown";
const tosContentLanguage = resp.headers.get("content-language") || undefined;
const tosContentType = resp.headers.get("content-type") || "text/plain";
const availLangStr = resp.headers.get("avail-languages") || "";
@@ -237,6 +269,83 @@ async function getExchangeRecordsInternal(
return details;
}
+export async function getScopeForAllCoins(
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ exs: string[],
+): Promise<ScopeInfo[]> {
+ const queries = exs.map((exchange) => {
+ return getExchangeScopeInfoOrUndefined(tx, exchange);
+ });
+ const rs = await Promise.all(queries);
+ return rs.filter((d): d is ScopeInfo => d !== undefined);
+}
+
+export async function getScopeForAllExchanges(
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ exs: string[],
+): Promise<ScopeInfo[]> {
+ const queries = exs.map((exchange) => {
+ return getExchangeScopeInfoOrUndefined(tx, exchange);
+ });
+ const rs = await Promise.all(queries);
+ return rs.filter((d): d is ScopeInfo => d !== undefined);
+}
+
+export async function getCoinScopeInfoOrUndefined(
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coins",
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ coinPub: string,
+): Promise<ScopeInfo | undefined> {
+ const coin = await tx.coins.get(coinPub);
+ if (!coin) {
+ return undefined;
+ }
+ const det = await getExchangeRecordsInternal(tx, coin.exchangeBaseUrl);
+ if (!det) {
+ return undefined;
+ }
+ return internalGetExchangeScopeInfo(tx, det);
+}
+
+export async function getExchangeScopeInfoOrUndefined(
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "globalCurrencyExchanges",
+ "globalCurrencyAuditors",
+ ]
+ >,
+ exchangeBaseUrl: string,
+): Promise<ScopeInfo | undefined> {
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ return undefined;
+ }
+ return internalGetExchangeScopeInfo(tx, det);
+}
+
export async function getExchangeScopeInfo(
tx: WalletDbReadOnlyTransaction<
[
@@ -301,12 +410,30 @@ async function internalGetExchangeScopeInfo(
};
}
+function getKycStatusFromReserveStatus(
+ status: ReserveRecordStatus,
+): ExchangeWalletKycStatus {
+ switch (status) {
+ case ReserveRecordStatus.Done:
+ return ExchangeWalletKycStatus.Done;
+ // FIXME: Do we handle the suspended state?
+ case ReserveRecordStatus.SuspendedLegiInit:
+ case ReserveRecordStatus.PendingLegiInit:
+ return ExchangeWalletKycStatus.LegiInit;
+ // FIXME: Do we handle the suspended state?
+ case ReserveRecordStatus.SuspendedLegi:
+ case ReserveRecordStatus.PendingLegi:
+ return ExchangeWalletKycStatus.Legi;
+ }
+}
+
async function makeExchangeListItem(
tx: WalletDbReadOnlyTransaction<
["globalCurrencyExchanges", "globalCurrencyAuditors"]
>,
r: ExchangeEntryRecord,
exchangeDetails: ExchangeDetailsRecord | undefined,
+ reserveRec: ReserveRecord | undefined,
lastError: TalerErrorDetail | undefined,
): Promise<ExchangeListItem> {
const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
@@ -321,7 +448,12 @@ async function makeExchangeListItem(
scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
}
- return {
+ let walletKycStatus: ExchangeWalletKycStatus | undefined =
+ reserveRec && reserveRec.status
+ ? getKycStatusFromReserveStatus(reserveRec.status)
+ : undefined;
+
+ const listItem: ExchangeListItem = {
exchangeBaseUrl: r.baseUrl,
masterPub: exchangeDetails?.masterPublicKey,
noFees: r.noFees ?? false,
@@ -329,6 +461,13 @@ async function makeExchangeListItem(
currency: exchangeDetails?.currency ?? r.presetCurrencyHint ?? "UNKNOWN",
exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
+ walletKycStatus,
+ walletKycReservePub: reserveRec?.reservePub,
+ // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response
+ walletKycUrl: reserveRec?.kycAccessToken
+ ? new URL(`kyc-spa/${reserveRec.kycAccessToken}`, r.baseUrl).href
+ : undefined,
+ walletKycAccessToken: reserveRec?.kycAccessToken,
tosStatus: getExchangeTosStatusFromRecord(r),
ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
@@ -342,6 +481,14 @@ async function makeExchangeListItem(
url: r.baseUrl,
},
};
+ switch (listItem.exchangeUpdateStatus) {
+ case ExchangeUpdateStatus.UnavailableUpdate:
+ if (r.unavailableReason) {
+ listItem.unavailableReason = r.unavailableReason;
+ }
+ break;
+ }
+ return listItem;
}
export interface ExchangeWireDetails {
@@ -351,6 +498,7 @@ export interface ExchangeWireDetails {
exchangeBaseUrl: string;
auditors: ExchangeAuditor[];
globalFees: ExchangeGlobalFees[];
+ reserveClosingDelay: TalerProtocolDuration;
}
export async function getExchangeWireDetailsInTx(
@@ -368,6 +516,7 @@ export async function getExchangeWireDetailsInTx(
exchangeBaseUrl: det.exchangeBaseUrl,
auditors: det.auditors,
globalFees: det.globalFees,
+ reserveClosingDelay: det.reserveClosingDelay,
};
}
@@ -379,6 +528,7 @@ export async function lookupExchangeByUri(
{
storeNames: [
"exchanges",
+ "reserves",
"exchangeDetails",
"operationRetries",
"globalCurrencyAuditors",
@@ -397,10 +547,18 @@ export async function lookupExchangeByUri(
const opRetryRecord = await tx.operationRetries.get(
TaskIdentifiers.forExchangeUpdate(exchangeRec),
);
+ let reserveRec: ReserveRecord | undefined = undefined;
+ if (exchangeRec.currentMergeReserveRowId != null) {
+ reserveRec = await tx.reserves.get(
+ exchangeRec.currentMergeReserveRowId,
+ );
+ checkDbInvariant(!!reserveRec, "reserve record not found");
+ }
return await makeExchangeListItem(
tx,
exchangeRec,
exchangeDetails,
+ reserveRec,
opRetryRecord?.lastError,
);
},
@@ -696,6 +854,10 @@ export interface ExchangeKeysDownloadResult {
globalFees: GlobalFees[];
accounts: ExchangeWireAccount[];
wireFees: { [methodName: string]: WireFeesJson[] };
+ currencySpecification?: CurrencySpecification;
+ walletBalanceLimits: AmountString[] | undefined;
+ hardLimits: AccountLimit[] | undefined;
+ zeroLimits: ZeroLimitedOperation[] | undefined;
}
/**
@@ -858,6 +1020,46 @@ async function downloadExchangeKeysInfo(
globalFees: exchangeKeysJsonUnchecked.global_fees,
accounts: exchangeKeysJsonUnchecked.accounts,
wireFees: exchangeKeysJsonUnchecked.wire_fees,
+ currencySpecification: exchangeKeysJsonUnchecked.currency_specification,
+ walletBalanceLimits:
+ exchangeKeysJsonUnchecked.wallet_balance_limit_without_kyc,
+ hardLimits: exchangeKeysJsonUnchecked.hard_limits,
+ zeroLimits: exchangeKeysJsonUnchecked.zero_limits,
+ };
+}
+
+type TosMetaResult = { type: "not-found" } | { type: "ok"; etag: string };
+
+/**
+ * Download metadata about an exchange's terms of service.
+ */
+async function downloadTosMeta(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TosMetaResult> {
+ logger.trace(`downloading exchange tos metadata for ${exchangeBaseUrl}`);
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+
+ // FIXME: We can/should make a HEAD request here.
+ // Not sure if qtart supports it at the moment.
+ const resp = await wex.http.fetch(reqUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.NotImplemented:
+ return { type: "not-found" };
+ case HttpStatusCode.Ok:
+ break;
+ default:
+ throwUnexpectedRequestError(resp, await readTalerErrorResponse(resp));
+ }
+
+ const etag = resp.headers.get("taler-terms-version") || "unknown";
+ return {
+ type: "ok",
+ etag,
};
}
@@ -900,6 +1102,36 @@ async function downloadTosFromAcceptedFormat(
}
/**
+ * Check if an exchange entry should be considered
+ * to be outdated.
+ */
+async function checkExchangeEntryOutdated(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["exchanges", "denominations"]>,
+ exchangeBaseUrl: string,
+): Promise<boolean> {
+ // We currently consider the exchange outdated when no
+ // denominations can be used for withdrawal.
+
+ logger.trace(`checking if exchange entry for ${exchangeBaseUrl} is outdated`);
+ let numOkay = 0;
+ let denoms =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ logger.trace(`exchange entry has ${denoms.length} denominations`);
+ for (const denom of denoms) {
+ const denomOkay = isWithdrawableDenom(
+ denom,
+ wex.ws.config.testing.denomselAllowLate,
+ );
+ if (denomOkay) {
+ numOkay++;
+ }
+ }
+ logger.trace(`Of these, ${numOkay} are usable`);
+ return numOkay === 0;
+}
+
+/**
* Transition an exchange into an updating state.
*
* If the update is forced, the exchange is put into an updating state
@@ -935,12 +1167,16 @@ async function startUpdateExchangeEntry(
const { oldExchangeState, newExchangeState, taskId } =
await wex.db.runReadWriteTx(
- { storeNames: ["exchanges", "operationRetries"] },
+ { storeNames: ["exchanges", "operationRetries", "denominations"] },
async (tx) => {
const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) {
throw Error("exchange not found");
}
+
+ // FIXME: Do not transition at all if the exchange info is recent enough
+ // and the request is not forced.
+
const oldExchangeState = getExchangeState(r);
switch (r.updateStatus) {
case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
@@ -949,7 +1185,21 @@ async function startUpdateExchangeEntry(
case ExchangeEntryDbUpdateStatus.Suspended:
r.cachebreakNextUpdate = options.forceUpdate;
break;
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate: {
+ const outdated = await checkExchangeEntryOutdated(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ );
+ if (outdated) {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.OutdatedUpdate;
+ } else {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ }
+ r.cachebreakNextUpdate = options.forceUpdate;
+ break;
+ }
+ case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
r.cachebreakNextUpdate = options.forceUpdate;
break;
case ExchangeEntryDbUpdateStatus.Ready: {
@@ -961,7 +1211,16 @@ async function startUpdateExchangeEntry(
options.forceUpdate ||
AbsoluteTime.isExpired(nextUpdateTimestamp)
) {
- r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ const outdated = await checkExchangeEntryOutdated(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ );
+ if (outdated) {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.OutdatedUpdate;
+ } else {
+ r.updateStatus = ExchangeEntryDbUpdateStatus.ReadyUpdate;
+ }
r.cachebreakNextUpdate = options.forceUpdate;
}
break;
@@ -1006,132 +1265,8 @@ export interface ReadyExchangeSummary {
protocolVersionRange: string;
tosAcceptedTimestamp: TalerPreciseTimestamp | undefined;
scopeInfo: ScopeInfo;
-}
-
-async function internalWaitReadyExchange(
- wex: WalletExecutionContext,
- canonUrl: string,
- exchangeNotifFlag: AsyncFlag,
- options: {
- cancellationToken?: CancellationToken;
- forceUpdate?: boolean;
- expectedMasterPub?: string;
- } = {},
-): Promise<ReadyExchangeSummary> {
- const operationId = constructTaskIdentifier({
- tag: PendingTaskType.ExchangeUpdate,
- exchangeBaseUrl: canonUrl,
- });
- while (true) {
- if (wex.cancellationToken.isCancelled) {
- throw Error("cancelled");
- }
- logger.info(`waiting for ready exchange ${canonUrl}`);
- const { exchange, exchangeDetails, retryInfo, scopeInfo } =
- await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
- },
- async (tx) => {
- const exchange = await tx.exchanges.get(canonUrl);
- const exchangeDetails = await getExchangeRecordsInternal(
- tx,
- canonUrl,
- );
- const retryInfo = await tx.operationRetries.get(operationId);
- let scopeInfo: ScopeInfo | undefined = undefined;
- if (exchange && exchangeDetails) {
- scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
- }
- return { exchange, exchangeDetails, retryInfo, scopeInfo };
- },
- );
-
- if (!exchange) {
- throw Error("exchange entry does not exist anymore");
- }
-
- let ready = false;
-
- switch (exchange.updateStatus) {
- case ExchangeEntryDbUpdateStatus.Ready:
- ready = true;
- break;
- case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- // If the update is forced,
- // we wait until we're in a full "ready" state,
- // as we're not happy with the stale information.
- if (!options.forceUpdate) {
- ready = true;
- }
- break;
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- default: {
- if (retryInfo) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
- {
- exchangeBaseUrl: canonUrl,
- innerError: retryInfo?.lastError,
- },
- );
- }
- }
- }
-
- if (!ready) {
- logger.info("waiting for exchange update notification");
- await exchangeNotifFlag.wait();
- logger.info("done waiting for exchange update notification");
- exchangeNotifFlag.reset();
- continue;
- }
-
- if (!exchangeDetails) {
- throw Error("invariant failed");
- }
-
- if (!scopeInfo) {
- throw Error("invariant failed");
- }
-
- const res: ReadyExchangeSummary = {
- currency: exchangeDetails.currency,
- exchangeBaseUrl: canonUrl,
- masterPub: exchangeDetails.masterPublicKey,
- tosStatus: getExchangeTosStatusFromRecord(exchange),
- tosAcceptedEtag: exchange.tosAcceptedEtag,
- wireInfo: exchangeDetails.wireInfo,
- protocolVersionRange: exchangeDetails.protocolVersionRange,
- tosCurrentEtag: exchange.tosCurrentEtag,
- tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
- exchange.tosAcceptedTimestamp,
- ),
- scopeInfo,
- };
-
- if (options.expectedMasterPub) {
- if (res.masterPub !== options.expectedMasterPub) {
- throw Error(
- "public key of the exchange does not match expected public key",
- );
- }
- }
- return res;
- }
+ zeroLimits: ZeroLimitedOperation[];
+ hardLimits: AccountLimit[];
}
/**
@@ -1186,39 +1321,134 @@ async function waitReadyExchange(
} = {},
): Promise<ReadyExchangeSummary> {
logger.trace(`waiting for exchange ${canonUrl} to become ready`);
- // FIXME: We should use Symbol.dispose magic here for cleanup!
- const exchangeNotifFlag = new AsyncFlag();
- // Raise exchangeNotifFlag whenever we get a notification
- // about our exchange.
- const cancelNotif = wex.ws.addNotificationListener((notif) => {
- if (
- notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === canonUrl
- ) {
- logger.info(`raising update notification: ${j2s(notif)}`);
- exchangeNotifFlag.raise();
- }
+ const operationId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeUpdate,
+ exchangeBaseUrl: canonUrl,
});
- const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
- cancelNotif();
- exchangeNotifFlag.raise();
+ let res: ReadyExchangeSummary | undefined = undefined;
+
+ await genericWaitForState(wex, {
+ filterNotification(notif): boolean {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === canonUrl
+ );
+ },
+ async checkState(): Promise<boolean> {
+ const { exchange, exchangeDetails, retryInfo, scopeInfo } =
+ await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(canonUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ canonUrl,
+ );
+ const retryInfo = await tx.operationRetries.get(operationId);
+ let scopeInfo: ScopeInfo | undefined = undefined;
+ if (exchange && exchangeDetails) {
+ scopeInfo = await internalGetExchangeScopeInfo(
+ tx,
+ exchangeDetails,
+ );
+ }
+ return { exchange, exchangeDetails, retryInfo, scopeInfo };
+ },
+ );
+
+ if (!exchange) {
+ throw Error("exchange entry does not exist anymore");
+ }
+
+ let ready = false;
+
+ switch (exchange.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ ready = true;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ // If the update is forced,
+ // we wait until we're in a full "ready" state,
+ // as we're not happy with the stale information.
+ if (!options.forceUpdate) {
+ ready = true;
+ }
+ break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
+ default: {
+ if (retryInfo) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
+ }
+ }
+ }
+
+ if (!ready) {
+ return false;
+ }
+
+ if (!exchangeDetails) {
+ throw Error("invariant failed");
+ }
+
+ if (!scopeInfo) {
+ throw Error("invariant failed");
+ }
+
+ const mySummary: ReadyExchangeSummary = {
+ currency: exchangeDetails.currency,
+ exchangeBaseUrl: canonUrl,
+ masterPub: exchangeDetails.masterPublicKey,
+ tosStatus: getExchangeTosStatusFromRecord(exchange),
+ tosAcceptedEtag: exchange.tosAcceptedEtag,
+ wireInfo: exchangeDetails.wireInfo,
+ protocolVersionRange: exchangeDetails.protocolVersionRange,
+ tosCurrentEtag: exchange.tosCurrentEtag,
+ tosAcceptedTimestamp: timestampOptionalPreciseFromDb(
+ exchange.tosAcceptedTimestamp,
+ ),
+ scopeInfo,
+ hardLimits: exchangeDetails.hardLimits ?? [],
+ zeroLimits: exchangeDetails.zeroLimits ?? [],
+ };
+
+ if (options.expectedMasterPub) {
+ if (mySummary.masterPub !== options.expectedMasterPub) {
+ throw Error(
+ "public key of the exchange does not match expected public key",
+ );
+ }
+ }
+ res = mySummary;
+ return true;
+ },
});
- try {
- const res = await internalWaitReadyExchange(
- wex,
- canonUrl,
- exchangeNotifFlag,
- options,
- );
- logger.info("done waiting for ready exchange");
- return res;
- } finally {
- unregisterOnCancelled();
- cancelNotif();
- }
+ checkLogicInvariant(!!res);
+ return res;
}
function checkPeerPaymentsDisabled(
@@ -1286,6 +1516,10 @@ export async function updateExchangeFromUrlHandler(
wex: WalletExecutionContext,
exchangeBaseUrl: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
logger.trace(`updating exchange info for ${exchangeBaseUrl}`);
const oldExchangeRec = await wex.db.runReadOnlyTx(
@@ -1309,6 +1543,7 @@ export async function updateExchangeFromUrlHandler(
case ExchangeEntryDbUpdateStatus.Initial:
logger.info(`not updating exchange in status "initial"`);
return TaskRunResult.finished();
+ case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
case ExchangeEntryDbUpdateStatus.InitialUpdate:
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
updateRequestedExplicitly = true;
@@ -1367,7 +1602,6 @@ export async function updateExchangeFromUrlHandler(
AbsoluteTime.min(nextUpdateStamp, nextRefreshCheckStamp),
);
}
-
}
// When doing the auto-refresh check, we always update
@@ -1423,15 +1657,7 @@ export async function updateExchangeFromUrlHandler(
logger.trace("finished validating exchange /wire info");
- // We download the text/plain version here,
- // because that one needs to exist, and we
- // will get the current etag from the response.
- const tosDownload = await downloadTosFromAcceptedFormat(
- wex,
- exchangeBaseUrl,
- timeout,
- ["text/plain"],
- );
+ const tosMeta = await downloadTosMeta(wex, exchangeBaseUrl);
logger.trace("updating exchange info in database");
@@ -1460,6 +1686,8 @@ export async function updateExchangeFromUrlHandler(
"recoupGroups",
"coinAvailability",
"denomLossEvents",
+ "currencyInfo",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1480,16 +1708,18 @@ export async function updateExchangeFromUrlHandler(
detailsPointerChanged = true;
}
let detailsIncompatible = false;
+ let conflictHint: string | undefined = undefined;
if (existingDetails) {
if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
detailsIncompatible = true;
detailsPointerChanged = true;
- }
- if (existingDetails.currency !== keysInfo.currency) {
+ conflictHint = "master public key changed";
+ } else if (existingDetails.currency !== keysInfo.currency) {
detailsIncompatible = true;
detailsPointerChanged = true;
+ conflictHint = "currency changed";
}
- // FIXME: We need to do some consistency checks!
+ // FIXME: We need to do some more consistency checks!
}
if (detailsIncompatible) {
logger.warn(
@@ -1498,6 +1728,12 @@ export async function updateExchangeFromUrlHandler(
// We don't support this gracefully right now.
// See https://bugs.taler.net/n/8576
r.updateStatus = ExchangeEntryDbUpdateStatus.UnavailableUpdate;
+ r.unavailableReason = makeTalerErrorDetail(
+ TalerErrorCode.WALLET_EXCHANGE_ENTRY_UPDATE_CONFLICT,
+ {
+ detail: conflictHint,
+ },
+ );
r.updateRetryCounter = (r.updateRetryCounter ?? 0) + 1;
r.nextUpdateStamp = computeDbBackoff(r.updateRetryCounter);
r.nextRefreshCheckStamp = timestampPreciseToDb(
@@ -1510,6 +1746,7 @@ export async function updateExchangeFromUrlHandler(
newExchangeState: getExchangeState(r),
};
}
+ delete r.unavailableReason;
r.updateRetryCounter = 0;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
@@ -1521,10 +1758,18 @@ export async function updateExchangeFromUrlHandler(
exchangeBaseUrl: r.baseUrl,
wireInfo,
ageMask,
+ walletBalanceLimits: keysInfo.walletBalanceLimits,
};
r.noFees = noFees;
r.peerPaymentsDisabled = peerPaymentsDisabled;
- r.tosCurrentEtag = tosDownload.tosEtag;
+ switch (tosMeta.type) {
+ case "not-found":
+ r.tosCurrentEtag = undefined;
+ break;
+ case "ok":
+ r.tosCurrentEtag = tosMeta.etag;
+ break;
+ }
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
@@ -1549,6 +1794,21 @@ export async function updateExchangeFromUrlHandler(
r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
+
+ if (keysInfo.currencySpecification) {
+ // Since this is the per-exchange currency info,
+ // we update it when the exchange changes it.
+ await WalletDbHelpers.upsertCurrencyInfo(tx, {
+ currencySpec: keysInfo.currencySpecification,
+ scopeInfo: {
+ type: ScopeType.Exchange,
+ currency: newDetails.currency,
+ url: exchangeBaseUrl,
+ },
+ source: "exchange",
+ });
+ }
+
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(
typeof drRowId.key === "number",
@@ -1659,87 +1919,8 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating exchange info in database");
- logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`);
-
- let minCheckThreshold = AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ days: 1 }),
- );
-
if (refreshCheckNecessary) {
- // Do auto-refresh.
- await wex.db.runReadWriteTx(
- {
- storeNames: [
- "coins",
- "denominations",
- "coinAvailability",
- "refreshGroups",
- "refreshSessions",
- "exchanges",
- ],
- },
- async (tx) => {
- const exchange = await tx.exchanges.get(exchangeBaseUrl);
- if (!exchange || !exchange.detailsPointer) {
- return;
- }
- const coins = await tx.coins.indexes.byBaseUrl
- .iter(exchangeBaseUrl)
- .toArray();
- const refreshCoins: CoinRefreshRequest[] = [];
- for (const coin of coins) {
- if (coin.status !== CoinStatus.Fresh) {
- continue;
- }
- const denom = await tx.denominations.get([
- exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- logger.warn("denomination not in database");
- continue;
- }
- const executeThreshold =
- getAutoRefreshExecuteThresholdForDenom(denom);
- if (AbsoluteTime.isExpired(executeThreshold)) {
- refreshCoins.push({
- coinPub: coin.coinPub,
- amount: denom.value,
- });
- } else {
- const checkThreshold = getAutoRefreshCheckThreshold(denom);
- minCheckThreshold = AbsoluteTime.min(
- minCheckThreshold,
- checkThreshold,
- );
- }
- }
- if (refreshCoins.length > 0) {
- const res = await createRefreshGroup(
- wex,
- tx,
- exchange.detailsPointer?.currency,
- refreshCoins,
- RefreshReason.Scheduled,
- undefined,
- );
- logger.trace(
- `created refresh group for auto-refresh (${res.refreshGroupId})`,
- );
- }
- logger.trace(
- `next refresh check at ${AbsoluteTime.toIsoString(
- minCheckThreshold,
- )}`,
- );
- exchange.nextRefreshCheckStamp = timestampPreciseToDb(
- AbsoluteTime.toPreciseTimestamp(minCheckThreshold),
- );
- wex.ws.exchangeCache.clear();
- await tx.exchanges.put(exchange);
- },
- );
+ await doAutoRefresh(wex, exchangeBaseUrl);
}
wex.ws.notify({
@@ -1754,6 +1935,90 @@ export async function updateExchangeFromUrlHandler(
return TaskRunResult.progress();
}
+async function doAutoRefresh(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`);
+
+ let minCheckThreshold = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 1 }),
+ );
+
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
+ "exchanges",
+ "refreshGroups",
+ "refreshSessions",
+ "transactionsMeta",
+ ],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange || !exchange.detailsPointer) {
+ return;
+ }
+ const coins = await tx.coins.indexes.byBaseUrl
+ .iter(exchangeBaseUrl)
+ .toArray();
+ const refreshCoins: CoinRefreshRequest[] = [];
+ for (const coin of coins) {
+ if (coin.status !== CoinStatus.Fresh) {
+ continue;
+ }
+ const denom = await tx.denominations.get([
+ exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ if (!denom) {
+ logger.warn("denomination not in database");
+ continue;
+ }
+ const executeThreshold = getAutoRefreshExecuteThresholdForDenom(denom);
+ if (AbsoluteTime.isExpired(executeThreshold)) {
+ refreshCoins.push({
+ coinPub: coin.coinPub,
+ amount: denom.value,
+ });
+ } else {
+ const checkThreshold = getAutoRefreshCheckThreshold(denom);
+ minCheckThreshold = AbsoluteTime.min(
+ minCheckThreshold,
+ checkThreshold,
+ );
+ }
+ }
+ if (refreshCoins.length > 0) {
+ const res = await createRefreshGroup(
+ wex,
+ tx,
+ exchange.detailsPointer?.currency,
+ refreshCoins,
+ RefreshReason.Scheduled,
+ undefined,
+ );
+ logger.trace(
+ `created refresh group for auto-refresh (${res.refreshGroupId})`,
+ );
+ }
+ logger.trace(
+ `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
+ );
+ exchange.nextRefreshCheckStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(minCheckThreshold),
+ );
+ wex.ws.exchangeCache.clear();
+ await tx.exchanges.put(exchange);
+ },
+ );
+}
+
interface DenomLossResult {
notifications: WalletNotification[];
}
@@ -1761,7 +2026,13 @@ interface DenomLossResult {
async function handleDenomLoss(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["coinAvailability", "denominations", "denomLossEvents", "coins"]
+ [
+ "coinAvailability",
+ "denominations",
+ "denomLossEvents",
+ "coins",
+ "transactionsMeta",
+ ]
>,
currency: string,
exchangeBaseUrl: string,
@@ -1852,13 +2123,11 @@ async function handleDenomLoss(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
});
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.DenomLoss,
- denomLossEventId,
- });
+ const ctx = new DenomLossTransactionContext(wex, denomLossEventId);
+ await ctx.updateTransactionMeta(tx);
result.notifications.push({
type: NotificationType.TransactionStateTransition,
- transactionId,
+ transactionId: ctx.transactionId,
oldTxState: {
major: TransactionMajorState.None,
},
@@ -1868,7 +2137,7 @@ async function handleDenomLoss(
});
result.notifications.push({
type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
+ hintTransactionId: ctx.transactionId,
});
}
@@ -1884,13 +2153,11 @@ async function handleDenomLoss(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
});
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.DenomLoss,
- denomLossEventId,
- });
+ const ctx = new DenomLossTransactionContext(wex, denomLossEventId);
+ await ctx.updateTransactionMeta(tx);
result.notifications.push({
type: NotificationType.TransactionStateTransition,
- transactionId,
+ transactionId: ctx.transactionId,
oldTxState: {
major: TransactionMajorState.None,
},
@@ -1900,7 +2167,7 @@ async function handleDenomLoss(
});
result.notifications.push({
type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
+ hintTransactionId: ctx.transactionId,
});
}
@@ -1955,23 +2222,55 @@ export function computeDenomLossTransactionStatus(
}
export class DenomLossTransactionContext implements TransactionContext {
+ transactionId: TransactionIdStr;
+
+ constructor(
+ private wex: WalletExecutionContext,
+ public denomLossEventId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ }
+
get taskId(): TaskIdStr | undefined {
return undefined;
}
- transactionId: TransactionIdStr;
+
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["denomLossEvents", "transactionsMeta"]>,
+ ): Promise<void> {
+ const denomLossRec = await tx.denomLossEvents.get(this.denomLossEventId);
+ if (!denomLossRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: denomLossRec.status,
+ timestamp: denomLossRec.timestampCreated,
+ currency: denomLossRec.currency,
+ exchanges: [denomLossRec.exchangeBaseUrl],
+ });
+ }
abortTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
suspendTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
resumeTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
failTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
async deleteTransaction(): Promise<void> {
const transitionInfo = await this.wex.db.runReadWriteTx(
{ storeNames: ["denomLossEvents"] },
@@ -1993,21 +2292,43 @@ export class DenomLossTransactionContext implements TransactionContext {
notifyTransition(this.wex, this.transactionId, transitionInfo);
}
- constructor(
- private wex: WalletExecutionContext,
- public denomLossEventId: string,
- ) {
- this.transactionId = constructTransactionIdentifier({
- tag: TransactionType.DenomLoss,
- denomLossEventId,
- });
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const rec = await tx.denomLossEvents.get(this.denomLossEventId);
+ if (!rec) {
+ return undefined;
+ }
+ const txState = computeDenomLossTransactionStatus(rec);
+ return {
+ type: TransactionType.DenomLoss,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [rec.exchangeBaseUrl]),
+ txActions: [TransactionAction.Delete],
+ amountRaw: Amounts.stringify(rec.amount),
+ amountEffective: Amounts.stringify(rec.amount),
+ timestamp: timestampPreciseFromDb(rec.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId: rec.denomLossEventId,
+ }),
+ lossEventType: rec.eventType,
+ exchangeBaseUrl: rec.exchangeBaseUrl,
+ };
}
}
async function handleRecoup(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["denominations", "coins", "recoupGroups", "refreshGroups"]
+ [
+ "denominations",
+ "coins",
+ "recoupGroups",
+ "refreshGroups",
+ "transactionsMeta",
+ "exchanges",
+ ]
>,
exchangeBaseUrl: string,
recoup: Recoup[],
@@ -2119,6 +2440,19 @@ export async function getExchangeTos(
): Promise<GetExchangeTosResult> {
const exch = await fetchFreshExchange(wex, exchangeBaseUrl);
+ switch (exch.tosStatus) {
+ case ExchangeTosStatus.MissingTos:
+ return {
+ tosStatus: ExchangeTosStatus.MissingTos,
+ acceptedEtag: undefined,
+ contentLanguage: undefined,
+ contentType: "text/plain",
+ content: "NULL",
+ currentEtag: "NULL",
+ tosAvailableLanguages: [],
+ };
+ }
+
const tosDownload = await downloadTosFromAcceptedFormat(
wex,
exchangeBaseUrl,
@@ -2187,6 +2521,7 @@ export async function listExchanges(
{
storeNames: [
"exchanges",
+ "reserves",
"operationRetries",
"exchangeDetails",
"globalCurrencyAuditors",
@@ -2195,18 +2530,29 @@ export async function listExchanges(
},
async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
- for (const r of exchangeRecords) {
+ for (const exchangeRec of exchangeRecords) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.ExchangeUpdate,
- exchangeBaseUrl: r.baseUrl,
+ exchangeBaseUrl: exchangeRec.baseUrl,
});
- const exchangeDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
+ const exchangeDetails = await getExchangeRecordsInternal(
+ tx,
+ exchangeRec.baseUrl,
+ );
const opRetryRecord = await tx.operationRetries.get(taskId);
+ let reserveRec: ReserveRecord | undefined = undefined;
+ if (exchangeRec.currentMergeReserveRowId != null) {
+ reserveRec = await tx.reserves.get(
+ exchangeRec.currentMergeReserveRowId,
+ );
+ checkDbInvariant(!!reserveRec, "reserve record not found");
+ }
exchanges.push(
await makeExchangeListItem(
tx,
- r,
+ exchangeRec,
exchangeDetails,
+ reserveRec,
opRetryRecord?.lastError,
),
);
@@ -2444,27 +2790,20 @@ async function internalGetExchangeResources(
* but keeps some transactions (payments, p2p, refreshes) around.
*/
async function purgeExchange(
- tx: WalletDbReadWriteTransaction<
- [
- "exchanges",
- "exchangeDetails",
- "transactions",
- "coinAvailability",
- "coins",
- "denominations",
- "exchangeSignKeys",
- "withdrawalGroups",
- "planchets",
- ]
- >,
+ wex: WalletExecutionContext,
+ tx: WalletDbAllStoresReadWriteTransaction,
exchangeBaseUrl: string,
): Promise<void> {
const detRecs = await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll();
+ // Remove all exchange detail records for that exchange
for (const r of detRecs) {
if (r.rowId == null) {
// Should never happen, as rowId is the primary key.
continue;
}
+ if (r.exchangeBaseUrl !== exchangeBaseUrl) {
+ continue;
+ }
await tx.exchangeDetails.delete(r.rowId);
const signkeyRecs =
await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(r.rowId);
@@ -2519,6 +2858,8 @@ async function purgeExchange(
}
}
}
+
+ await rematerializeTransactions(wex, tx);
}
export async function deleteExchange(
@@ -2527,36 +2868,21 @@ export async function deleteExchange(
): Promise<void> {
let inUse: boolean = false;
const exchangeBaseUrl = req.exchangeBaseUrl;
- await wex.db.runReadWriteTx(
- {
- storeNames: [
- "exchanges",
- "exchangeDetails",
- "transactions",
- "coinAvailability",
- "coins",
- "denominations",
- "exchangeSignKeys",
- "withdrawalGroups",
- "planchets",
- ],
- },
- async (tx) => {
- const exchangeRec = await tx.exchanges.get(exchangeBaseUrl);
- if (!exchangeRec) {
- // Nothing to delete!
- logger.info("no exchange found to delete");
- return;
- }
- const res = await internalGetExchangeResources(wex, tx, exchangeBaseUrl);
- if (res.hasResources && !req.purge) {
- inUse = true;
- return;
- }
- await purgeExchange(tx, exchangeBaseUrl);
- wex.ws.exchangeCache.clear();
- },
- );
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ const exchangeRec = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchangeRec) {
+ // Nothing to delete!
+ logger.info("no exchange found to delete");
+ return;
+ }
+ const res = await internalGetExchangeResources(wex, tx, exchangeBaseUrl);
+ if (res.hasResources && !req.purge) {
+ inUse = true;
+ return;
+ }
+ await purgeExchange(wex, tx, exchangeBaseUrl);
+ wex.ws.exchangeCache.clear();
+ });
if (inUse) {
throw TalerError.fromUncheckedDetail({
@@ -2586,3 +2912,673 @@ export async function getExchangeResources(
}
return res;
}
+
+/**
+ * Find the currently applicable wire fee for an exchange.
+ */
+export async function getExchangeWireFee(
+ wex: WalletExecutionContext,
+ wireType: string,
+ baseUrl: string,
+ time: TalerProtocolTimestamp,
+): Promise<WireFee> {
+ const exchangeDetails = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const ex = await tx.exchanges.get(baseUrl);
+ if (!ex || !ex.detailsPointer) return undefined;
+ return await tx.exchangeDetails.indexes.byPointer.get([
+ baseUrl,
+ ex.detailsPointer.currency,
+ ex.detailsPointer.masterPublicKey,
+ ]);
+ },
+ );
+
+ if (!exchangeDetails) {
+ throw Error(`exchange missing: ${baseUrl}`);
+ }
+
+ const fees = exchangeDetails.wireInfo.feesForType[wireType];
+ if (!fees || fees.length === 0) {
+ throw Error(
+ `exchange ${baseUrl} doesn't have fees for wire type ${wireType}`,
+ );
+ }
+ const fee = fees.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.fromProtocolTimestamp(time),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ });
+ if (!fee) {
+ throw Error(
+ `exchange ${exchangeDetails.exchangeBaseUrl} doesn't have fees for wire type ${wireType} at ${time.t_s}`,
+ );
+ }
+
+ return fee;
+}
+
+export type BalanceThresholdCheckResult =
+ | {
+ result: "ok";
+ }
+ | {
+ result: "violation";
+ nextThreshold: AmountString;
+ walletKycStatus: ExchangeWalletKycStatus | undefined;
+ walletKycAccessToken: string | undefined;
+ };
+
+export async function checkIncomingAmountLegalUnderKycBalanceThreshold(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ amountIncoming: AmountLike,
+): Promise<BalanceThresholdCheckResult> {
+ logger.info(`checking ${exchangeBaseUrl} +${amountIncoming} for KYC`);
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "reserves",
+ "coinAvailability",
+ ],
+ },
+ async (tx): Promise<BalanceThresholdCheckResult> => {
+ const exchangeRec = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchangeRec) {
+ throw Error("exchange not found");
+ }
+ const det = await getExchangeRecordsInternal(tx, exchangeBaseUrl);
+ if (!det) {
+ throw Error("exchange not found");
+ }
+ const coinAvRecs =
+ await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ let balAmount = Amounts.zeroOfCurrency(det.currency);
+ for (const av of coinAvRecs) {
+ const n = av.freshCoinCount + (av.pendingRefreshOutputCount ?? 0);
+ balAmount = Amounts.add(
+ balAmount,
+ Amounts.mult(av.value, n).amount,
+ ).amount;
+ }
+ const balExpected = Amounts.add(balAmount, amountIncoming).amount;
+
+ // Check if we already have KYC for a sufficient threshold.
+
+ const reserveId = exchangeRec.currentMergeReserveRowId;
+ let reserveRec: ReserveRecord | undefined;
+ if (reserveId) {
+ reserveRec = await tx.reserves.get(reserveId);
+ checkDbInvariant(!!reserveRec, "reserve");
+ // FIXME: also consider KYC expiration!
+ if (reserveRec.thresholdNext) {
+ if (Amounts.cmp(reserveRec.thresholdNext, balExpected) >= 0) {
+ return {
+ result: "ok",
+ };
+ }
+ } else if (reserveRec.status === ReserveRecordStatus.Done) {
+ // We don't know what the next threshold is, but we've passed *some* KYC
+ // check. We don't have enough information, so we allow the balance increase.
+ return {
+ result: "ok",
+ };
+ }
+ }
+
+ // No luck, check the next limit we should request, if any.
+
+ const limits = det.walletBalanceLimits;
+ if (!limits) {
+ logger.info("no balance limits defined");
+ return {
+ result: "ok",
+ };
+ }
+ limits.sort((a, b) => Amounts.cmp(a, b));
+ logger.info(`applicable limits: ${j2s(limits)}`);
+ let limViolated: AmountString | undefined = undefined;
+ let limNext: AmountString | undefined = undefined;
+ for (let i = 0; i < limits.length; i++) {
+ if (Amounts.cmp(limits[i], balExpected) <= 0) {
+ limViolated = limits[i];
+ limNext = limits[i + 1];
+ if (limNext == null || Amounts.cmp(limNext, balExpected) > 0) {
+ break;
+ }
+ }
+ }
+ if (!limViolated) {
+ logger.info("balance limit okay");
+ return {
+ result: "ok",
+ };
+ } else {
+ logger.info(
+ `balance limit ${limViolated} would be violated, next is ${limNext}`,
+ );
+ return {
+ result: "violation",
+ nextThreshold: limNext ?? limViolated,
+ walletKycStatus: reserveRec?.status
+ ? getKycStatusFromReserveStatus(reserveRec.status)
+ : undefined,
+ walletKycAccessToken: reserveRec?.kycAccessToken,
+ };
+ }
+ },
+ );
+}
+
+/**
+ * Wait until kyc has passed for the wallet.
+ *
+ * If passed==false, already return when legitimization
+ * is requested.
+ */
+export async function waitExchangeWalletKyc(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ passed: boolean,
+): Promise<void> {
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["exchanges", "reserves"],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ throw new Error("exchange not found");
+ }
+ const reserveId = exchange.currentMergeReserveRowId;
+ if (reserveId == null) {
+ logger.warn("KYC does not exist yet");
+ return false;
+ }
+ const reserve = await tx.reserves.get(reserveId);
+ if (!reserve) {
+ throw Error("reserve not found");
+ }
+ if (passed) {
+ if (
+ reserve.thresholdGranted &&
+ Amounts.cmp(reserve.thresholdGranted, amount) >= 0
+ ) {
+ return true;
+ }
+ return false;
+ } else {
+ if (
+ reserve.thresholdGranted &&
+ Amounts.cmp(reserve.thresholdGranted, amount) >= 0
+ ) {
+ return true;
+ }
+ if (reserve.status === ReserveRecordStatus.PendingLegi) {
+ return true;
+ }
+ return false;
+ }
+ },
+ );
+ },
+ filterNotification(notif) {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl
+ );
+ },
+ });
+}
+
+export async function handleTestingWaitExchangeState(
+ wex: WalletExecutionContext,
+ req: TestingWaitExchangeStateRequest,
+): Promise<EmptyObject> {
+ await genericWaitForState(wex, {
+ async checkState(): Promise<boolean> {
+ const exchangeEntry = await lookupExchangeByUri(wex, {
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ });
+ if (req.walletKycStatus) {
+ if (req.walletKycStatus !== exchangeEntry.walletKycStatus) {
+ return false;
+ }
+ }
+ return true;
+ },
+ filterNotification(notif) {
+ return (
+ notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === req.exchangeBaseUrl
+ );
+ },
+ });
+ return {};
+}
+
+export async function handleTestingWaitExchangeWalletKyc(
+ wex: WalletExecutionContext,
+ req: TestingWaitWalletKycRequest,
+): Promise<EmptyObject> {
+ await waitExchangeWalletKyc(wex, req.exchangeBaseUrl, req.amount, req.passed);
+ return {};
+}
+
+export async function handleStartExchangeWalletKyc(
+ wex: WalletExecutionContext,
+ req: StartExchangeWalletKycRequest,
+): Promise<EmptyObject> {
+ const newReservePair = await wex.cryptoApi.createEddsaKeypair({});
+ const dbRes = await wex.db.runReadWriteTx(
+ {
+ storeNames: ["exchanges", "reserves"],
+ },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(req.exchangeBaseUrl);
+ if (!exchange) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(exchange);
+ let mergeReserveRowId = exchange.currentMergeReserveRowId;
+ if (mergeReserveRowId == null) {
+ const putRes = await tx.reserves.put({
+ reservePriv: newReservePair.priv,
+ reservePub: newReservePair.pub,
+ });
+ checkDbInvariant(typeof putRes.key === "number", "primary key type");
+ mergeReserveRowId = putRes.key;
+ exchange.currentMergeReserveRowId = mergeReserveRowId;
+ await tx.exchanges.put(exchange);
+ }
+ const reserveRec = await tx.reserves.get(mergeReserveRowId);
+ checkDbInvariant(reserveRec != null, "reserve record exists");
+ if (
+ reserveRec.thresholdGranted == null ||
+ Amounts.cmp(reserveRec.thresholdGranted, req.amount) < 0
+ ) {
+ if (
+ reserveRec.thresholdRequested == null ||
+ Amounts.cmp(reserveRec.thresholdRequested, req.amount) < 0
+ ) {
+ reserveRec.thresholdRequested = req.amount;
+ reserveRec.status = ReserveRecordStatus.PendingLegiInit;
+ await tx.reserves.put(reserveRec);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchange.baseUrl,
+ oldExchangeState,
+ newExchangeState: getExchangeState(exchange),
+ } satisfies WalletNotification,
+ };
+ } else {
+ logger.info(
+ `another KYC process is already active for ${req.exchangeBaseUrl} over ${reserveRec.thresholdRequested}`,
+ );
+ return undefined;
+ }
+ } else {
+ // FIXME: Check expiration once exchange tells us!
+ logger.info(
+ `KYC already granted for ${req.exchangeBaseUrl} over ${req.amount}, granted ${reserveRec.thresholdGranted}`,
+ );
+ return undefined;
+ }
+ },
+ );
+ if (dbRes && dbRes.notification) {
+ wex.ws.notify(dbRes.notification);
+ }
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeWalletKyc,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ });
+ wex.taskScheduler.startShepherdTask(taskId);
+ return {};
+}
+
+async function handleExchangeKycPendingWallet(
+ wex: WalletExecutionContext,
+ exchange: ExchangeEntryRecord,
+ reserve: ReserveRecord,
+): Promise<TaskRunResult> {
+ checkDbInvariant(!!reserve.thresholdRequested, "threshold");
+ const threshold = reserve.thresholdRequested;
+ const sigResp = await wex.cryptoApi.signWalletAccountSetup({
+ reservePriv: reserve.reservePriv,
+ reservePub: reserve.reservePub,
+ threshold,
+ });
+ const requestUrl = new URL("kyc-wallet", exchange.baseUrl);
+ const body: WalletKycRequest = {
+ balance: reserve.thresholdRequested,
+ reserve_pub: reserve.reservePub,
+ reserve_sig: sigResp.sig,
+ };
+ logger.info(`kyc-wallet request body: ${j2s(body)}`);
+ const res = await wex.http.fetch(requestUrl.href, {
+ method: "POST",
+ body,
+ });
+
+ logger.info(`kyc-wallet response status is ${res.status}`);
+
+ switch (res.status) {
+ case HttpStatusCode.Ok: {
+ // KYC somehow already passed
+ // FIXME: Store next threshold and timestamp!
+ const accountKycStatus = await readSuccessResponseJsonOrThrow(
+ res,
+ codecForAccountKycStatus(),
+ );
+ return handleExchangeKycSuccess(wex, exchange.baseUrl, accountKycStatus);
+ }
+ case HttpStatusCode.NoContent: {
+ // KYC disabled at exchange.
+ return handleExchangeKycSuccess(wex, exchange.baseUrl, undefined);
+ }
+ case HttpStatusCode.Forbidden: {
+ // Did not work!
+ const err = await readTalerErrorResponse(res);
+ throwUnexpectedRequestError(res, err);
+ }
+ case HttpStatusCode.UnavailableForLegalReasons: {
+ const kycBody = await readResponseJsonOrThrow(
+ res,
+ codecForLegitimizationNeededResponse(),
+ );
+ return handleExchangeKycRespLegi(wex, exchange.baseUrl, reserve, kycBody);
+ }
+ default: {
+ const err = await readTalerErrorResponse(res);
+ throwUnexpectedRequestError(res, err);
+ }
+ }
+}
+
+async function handleExchangeKycSuccess(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ accountKycStatus: AccountKycStatus | undefined,
+): Promise<TaskRunResult> {
+ logger.info(`kyc check for ${exchangeBaseUrl} satisfied`);
+ const dbRes = await wex.db.runReadWriteTx(
+ { storeNames: ["exchanges", "reserves"] },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(exchange);
+ const reserveId = exchange.currentMergeReserveRowId;
+ if (reserveId == null) {
+ throw Error("expected exchange to have reserve ID");
+ }
+ const reserve = await tx.reserves.get(reserveId);
+ checkDbInvariant(!!reserve, "merge reserve should exist");
+ switch (reserve.status) {
+ case ReserveRecordStatus.PendingLegiInit:
+ case ReserveRecordStatus.PendingLegi:
+ break;
+ default:
+ throw Error("unexpected state (concurrent modification?)");
+ }
+ reserve.status = ReserveRecordStatus.Done;
+ reserve.thresholdGranted = reserve.thresholdRequested;
+ delete reserve.thresholdRequested;
+ delete reserve.requirementRow;
+
+ // Try to figure out the next balance limit
+ let nextLimit: AmountString | undefined = undefined;
+ if (accountKycStatus?.limits) {
+ for (const lim of accountKycStatus.limits) {
+ if (lim.operation_type.toLowerCase() === "balance") {
+ nextLimit = lim.threshold;
+ }
+ }
+ }
+ reserve.thresholdNext = nextLimit;
+
+ await tx.reserves.put(reserve);
+ logger.info(`newly granted threshold: ${reserve.thresholdGranted}`);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchange.baseUrl,
+ oldExchangeState,
+ newExchangeState: getExchangeState(exchange),
+ } satisfies WalletNotification,
+ };
+ },
+ );
+ if (dbRes && dbRes.notification) {
+ wex.ws.notify(dbRes.notification);
+ }
+ return TaskRunResult.progress();
+}
+
+/**
+ * The exchange has just told us that we need some legitimization
+ * from the user. Request more details and store the result in the database.
+ */
+async function handleExchangeKycRespLegi(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ reserve: ReserveRecord,
+ kycBody: LegitimizationNeededResponse,
+): Promise<TaskRunResult> {
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: reserve.reservePriv,
+ accountPub: reserve.reservePub,
+ });
+ const reqUrl = new URL(`kyc-check/${kycBody.h_payto}`, exchangeBaseUrl);
+ const resp = await wex.http.fetch(reqUrl.href, {
+ method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ });
+
+ logger.info(`kyc-check (long-poll) response status ${resp.status}`);
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ // FIXME: Store information about next limit!
+ const accountKycStatus = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycStatus(),
+ );
+ return handleExchangeKycSuccess(wex, exchangeBaseUrl, accountKycStatus);
+ }
+ case HttpStatusCode.Accepted: {
+ // Store the result in the DB!
+ break;
+ }
+ case HttpStatusCode.NoContent: {
+ // KYC not configured, so already satisfied
+ return handleExchangeKycSuccess(wex, exchangeBaseUrl, undefined);
+ }
+ default: {
+ const err = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, err);
+ }
+ }
+
+ const accountKycStatusResp = await readResponseJsonOrThrow(
+ resp,
+ codecForAccountKycStatus(),
+ );
+
+ const dbRes = await wex.db.runReadWriteTx(
+ { storeNames: ["exchanges", "reserves"] },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ throw Error("exchange not found");
+ }
+ const oldExchangeState = getExchangeState(exchange);
+ const reserveId = exchange.currentMergeReserveRowId;
+ if (reserveId == null) {
+ throw Error("expected exchange to have reserve ID");
+ }
+ const reserve = await tx.reserves.get(reserveId);
+ checkDbInvariant(!!reserve, "merge reserve should exist");
+ switch (reserve.status) {
+ case ReserveRecordStatus.PendingLegiInit:
+ break;
+ default:
+ throw Error("unexpected state (concurrent modification?)");
+ }
+ reserve.status = ReserveRecordStatus.PendingLegi;
+ reserve.requirementRow = kycBody.requirement_row;
+ reserve.amlReview = accountKycStatusResp.aml_review;
+ reserve.kycAccessToken = accountKycStatusResp.access_token;
+
+ await tx.reserves.put(reserve);
+ return {
+ notification: {
+ type: NotificationType.ExchangeStateTransition,
+ exchangeBaseUrl: exchange.baseUrl,
+ oldExchangeState,
+ newExchangeState: getExchangeState(exchange),
+ } satisfies WalletNotification,
+ };
+ },
+ );
+ if (dbRes && dbRes.notification) {
+ wex.ws.notify(dbRes.notification);
+ }
+ return TaskRunResult.progress();
+}
+
+/**
+ * Legitimization was requested from the user by the exchange.
+ *
+ * Long-poll for the legitimization to succeed.
+ */
+async function handleExchangeKycPendingLegitimization(
+ wex: WalletExecutionContext,
+ exchange: ExchangeEntryRecord,
+ reserve: ReserveRecord,
+): Promise<TaskRunResult> {
+ // FIXME: Cache this signature
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: reserve.reservePriv,
+ accountPub: reserve.reservePub,
+ });
+
+ const reservePayto = stringifyReservePaytoUri(
+ exchange.baseUrl,
+ reserve.reservePub,
+ );
+
+ const paytoHash = encodeCrock(hashPaytoUri(reservePayto));
+
+ const resp = await wex.ws.runLongpollQueueing(
+ wex,
+ exchange.baseUrl,
+ async (timeoutMs) => {
+ const reqUrl = new URL(`kyc-check/${paytoHash}`, exchange.baseUrl);
+ reqUrl.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`long-polling wallet KYC status at ${reqUrl.href}`);
+ return await wex.http.fetch(reqUrl.href, {
+ method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ });
+ },
+ );
+
+ logger.info(`kyc-check (long-poll) response status ${resp.status}`);
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ // FIXME: Store information about next limit!
+ const accountKycStatus = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycStatus(),
+ );
+ return handleExchangeKycSuccess(wex, exchange.baseUrl, accountKycStatus);
+ }
+ case HttpStatusCode.Accepted:
+ // FIXME: Do we ever need to update the access token?
+ return TaskRunResult.longpollReturnedPending();
+ case HttpStatusCode.NoContent: {
+ // KYC not configured, so already satisfied
+ return handleExchangeKycSuccess(wex, exchange.baseUrl, undefined);
+ }
+ default: {
+ const err = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, err);
+ }
+ }
+}
+
+export async function processExchangeKyc(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+): Promise<TaskRunResult> {
+ const res = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "reserves"] },
+ async (tx) => {
+ const exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ return undefined;
+ }
+ const reserveId = exchange.currentMergeReserveRowId;
+ let reserve: ReserveRecord | undefined = undefined;
+ if (reserveId != null) {
+ reserve = await tx.reserves.get(reserveId);
+ }
+ return { exchange, reserve };
+ },
+ );
+ if (!res) {
+ logger.warn(`exchange ${exchangeBaseUrl} not found, not processing KYC`);
+ return TaskRunResult.finished();
+ }
+ if (!res.reserve) {
+ return TaskRunResult.finished();
+ }
+ switch (res.reserve.status) {
+ case undefined:
+ // No KYC requested
+ return TaskRunResult.finished();
+ case ReserveRecordStatus.Done:
+ return TaskRunResult.finished();
+ case ReserveRecordStatus.SuspendedLegiInit:
+ case ReserveRecordStatus.SuspendedLegi:
+ return TaskRunResult.finished();
+ case ReserveRecordStatus.PendingLegiInit:
+ return handleExchangeKycPendingWallet(wex, res.exchange, res.reserve);
+ case ReserveRecordStatus.PendingLegi:
+ return handleExchangeKycPendingLegitimization(
+ wex,
+ res.exchange,
+ res.reserve,
+ );
+ }
+}
+
+export async function checkExchangeInScope(
+ wex: WalletExecutionContext,
+ exchangeBaseUrl: string,
+ scope: ScopeInfo,
+): Promise<boolean> {
+ if (scope.type === ScopeType.Exchange && scope.url !== exchangeBaseUrl) {
+ return false;
+ }
+ return true;
+}
diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts
index ec026b296..a7f0a5738 100644
--- a/packages/taler-wallet-core/src/host-impl.node.ts
+++ b/packages/taler-wallet-core/src/host-impl.node.ts
@@ -108,14 +108,22 @@ async function makeFileDb(
async function makeSqliteDb(
args: DefaultNodeWalletArgs,
): Promise<MakeDbResult> {
- BridgeIDBFactory.enableTracing = false;
+ if (process.env.TALER_WALLET_DBTRACING) {
+ BridgeIDBFactory.enableTracing = true;
+ } else {
+ BridgeIDBFactory.enableTracing = false;
+ }
const imp = await createNodeSqlite3Impl();
const dbFilename = args.persistentStoragePath ?? ":memory:";
logger.info(`using database ${dbFilename}`);
const myBackend = await createSqliteBackend(imp, {
filename: dbFilename,
});
- myBackend.enableTracing = false;
+ if (process.env.TALER_WALLET_DBTRACING) {
+ myBackend.enableTracing = true;
+ } else {
+ myBackend.enableTracing = false;
+ }
if (process.env.TALER_WALLET_DBSTATS) {
myBackend.trackStats = true;
}
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index fe2d3af15..30e9818d3 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -20,6 +20,7 @@
export * from "./crypto/cryptoImplementation.js";
export * from "./crypto/cryptoTypes.js";
+export * from "./crypto/index.js";
export {
CryptoDispatcher,
CryptoWorkerFactory,
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
index 03e702568..17c2439c7 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
@@ -26,7 +26,6 @@ import {
CoinInfo,
convertDepositAmountForAvailableCoins,
convertWithdrawalAmountFromAvailableCoins,
- getMaxDepositAmountForAvailableCoins,
} from "./instructedAmountConversion.js";
function makeCurrencyHelper(currency: string) {
@@ -254,76 +253,6 @@ test("deposit with wire fee raw 2", (t) => {
*
*/
-test("deposit max 35", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`0.00`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "34.9");
- t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max 35 with wirefee", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 5],
- [kudos`5`, 5],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`1`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "33.9");
- t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max repeated denom", (t) => {
- const coinList: Coin[] = [
- [kudos`2`, 1],
- [kudos`2`, 1],
- [kudos`5`, 1],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- "2": {
- wireFee: kudos`0.00`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.raw), "8.97");
- t.is(Amounts.stringifyValue(result.effective), "9");
-});
-
/**
* Making a withdrawal with effective amount
*
@@ -663,42 +592,6 @@ test("demo: withdraw raw 25", (t) => {
//shows fee = 0.2
});
-test("demo: deposit max after withdraw raw 25", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 8],
- [kudos`1`, 0],
- [kudos`2`, 2],
- [kudos`5`, 0],
- [kudos`10`, 2],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.01`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.effective), "24.8");
- t.is(Amounts.stringifyValue(result.raw), "24.67");
-
- // 8 x 0.1
- // 2 x 0.2
- // 2 x 10.0
- // total effective 24.8
- // deposit fee 12 x 0.01 = 0.12
- // wire fee 0.01
- // total raw: 24.8 - 0.13 = 24.67
-
- // current wallet impl fee 0.14
-});
-
test("demo: withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
@@ -729,39 +622,3 @@ test("demo: withdraw raw 13", (t) => {
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
-
-test("demo: deposit max after withdraw raw 13", (t) => {
- const coinList: Coin[] = [
- [kudos`0.1`, 8],
- [kudos`1`, 0],
- [kudos`2`, 1],
- [kudos`5`, 0],
- [kudos`10`, 1],
- ];
- const result = getMaxDepositAmountForAvailableCoins(
- {
- list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
- exchanges: {
- one: {
- wireFee: kudos`0.01`,
- purseFee: kudos`0.00`,
- creditDeadline: AbsoluteTime.never(),
- debitDeadline: AbsoluteTime.never(),
- },
- },
- },
- "KUDOS",
- );
- t.is(Amounts.stringifyValue(result.effective), "12.8");
- t.is(Amounts.stringifyValue(result.raw), "12.69");
-
- // 8 x 0.1
- // 1 x 0.2
- // 1 x 10.0
- // total effective 12.8
- // deposit fee 10 x 0.01 = 0.10
- // wire fee 0.01
- // total raw: 12.8 - 0.11 = 12.69
-
- // current wallet impl fee 0.14
-});
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index 5b399a0a7..c0f835dfa 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -23,12 +23,9 @@ import {
Amounts,
ConvertAmountRequest,
Duration,
- GetAmountRequest,
- GetPlanForOperationRequest,
TransactionAmountMode,
TransactionType,
checkDbInvariant,
- parsePaytoUri,
strcmp,
} from "@gnu-taler/taler-util";
import { DenominationRecord, timestampProtocolFromDb } from "./db.js";
@@ -85,26 +82,6 @@ interface SelectedCoins {
refresh?: RefreshChoice;
}
-function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
- switch (req.type) {
- case TransactionType.Withdrawal: {
- return {
- exchanges:
- req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
- };
- }
- case TransactionType.Deposit: {
- const payto = parsePaytoUri(req.account);
- if (!payto) {
- throw Error(`wrong payto ${req.account}`);
- }
- return {
- wireMethod: payto.targetType,
- };
- }
- }
-}
-
interface RefreshChoice {
/**
* Amount that need to be covered
@@ -141,13 +118,13 @@ interface AvailableCoins {
* This function is costly (by the database access) but with high chances
* of being cached
*/
-async function getAvailableDenoms(
+async function getAvailableCoins(
wex: WalletExecutionContext,
op: TransactionType,
currency: string,
filters: CoinsFilter = {},
): Promise<AvailableCoins> {
- const operationType = getOperationType(TransactionType.Deposit);
+ const operationType = getOperationType(op);
return await wex.db.runReadOnlyTx(
{
@@ -283,7 +260,10 @@ async function getAvailableDenoms(
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
- checkDbInvariant(!!denom, `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`);
+ checkDbInvariant(
+ !!denom,
+ `denomination of a coin is missing hash: ${coinAvail.denomPubHash}`,
+ );
if (denom.isRevoked || !denom.isOffered) {
continue;
}
@@ -354,7 +334,7 @@ export async function convertDepositAmount(
const amount = Amounts.parseOrThrow(req.amount);
// const filter = getCoinsFilter(req);
- const denoms = await getAvailableDenoms(
+ const denoms = await getAvailableCoins(
wex,
TransactionType.Deposit,
amount.currency,
@@ -374,6 +354,7 @@ export async function convertDepositAmount(
const LOG_REFRESH = false;
const LOG_DEPOSIT = false;
+
export function convertDepositAmountForAvailableCoins(
denoms: AvailableCoins,
amount: AmountJson,
@@ -420,9 +401,7 @@ export function convertDepositAmountForAvailableCoins(
}
const refreshDenoms = rankDenominationForRefresh(denoms.list);
- /**
- * FIXME: looking for refresh AFTER selecting greedy is not optimal
- */
+ // FIXME: looking for refresh AFTER selecting greedy is not optimal
const refreshCoin = searchBestRefreshCoin(
depositDenoms,
refreshDenoms,
@@ -437,7 +416,7 @@ export function convertDepositAmountForAvailableCoins(
refreshCoin.refreshEffective,
).amount;
const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
- //found with change
+ // found with change
return {
effective,
raw,
@@ -449,70 +428,13 @@ export function convertDepositAmountForAvailableCoins(
return result;
}
-export async function getMaxDepositAmount(
- wex: WalletExecutionContext,
- req: GetAmountRequest,
-): Promise<AmountResponse> {
- // const filter = getCoinsFilter(req);
-
- const denoms = await getAvailableDenoms(
- wex,
- TransactionType.Deposit,
- req.currency,
- {},
- );
-
- const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
- return {
- effectiveAmount: Amounts.stringify(result.effective),
- rawAmount: Amounts.stringify(result.raw),
- };
-}
-
-export function getMaxDepositAmountForAvailableCoins(
- denoms: AvailableCoins,
- currency: string,
-): AmountWithFee {
- const zero = Amounts.zeroOfCurrency(currency);
- if (!denoms.list.length) {
- // no coins in the database
- return { effective: zero, raw: zero };
- }
-
- const result = getTotalEffectiveAndRawForDeposit(
- denoms.list.map((info) => {
- return { info, size: info.totalAvailable ?? 0 };
- }),
- currency,
- );
-
- const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
- result.raw = Amounts.sub(result.raw, wireFee).amount;
-
- return result;
-}
-
-export async function convertPeerPushAmount(
- wex: WalletExecutionContext,
- req: ConvertAmountRequest,
-): Promise<AmountResponse> {
- throw Error("to be implemented after 1.0");
-}
-
-export async function getMaxPeerPushAmount(
- wex: WalletExecutionContext,
- req: GetAmountRequest,
-): Promise<AmountResponse> {
- throw Error("to be implemented after 1.0");
-}
-
export async function convertWithdrawalAmount(
wex: WalletExecutionContext,
req: ConvertAmountRequest,
): Promise<AmountResponse> {
const amount = Amounts.parseOrThrow(req.amount);
- const denoms = await getAvailableDenoms(
+ const denoms = await getAvailableCoins(
wex,
TransactionType.Withdrawal,
amount.currency,
@@ -553,14 +475,6 @@ export function convertWithdrawalAmountFromAvailableCoins(
* *****************************************************
*/
-/**
- *
- * @param depositDenoms
- * @param refreshDenoms
- * @param amount
- * @param mode
- * @returns
- */
function searchBestRefreshCoin(
depositDenoms: SelectableElement[],
refreshDenoms: Record<string, SelectableElement[]>,
@@ -643,18 +557,13 @@ function searchBestRefreshCoin(
/**
* Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
*/
function rankDenominationForWithdrawals(
denoms: CoinInfo[],
mode: TransactionAmountMode,
): SelectableElement[] {
const copyList = [...denoms];
- /**
- * Rank coins
- */
+ /// Rank coins
copyList.sort((d1, d2) => {
// the best coin to use is
// 1.- the one that contrib more and pay less fee
@@ -681,8 +590,8 @@ function rankDenominationForWithdrawals(
return copyList.map((info) => {
switch (mode) {
case TransactionAmountMode.Effective: {
- //if the user instructed "effective" then we need to selected
- //greedy total coin value
+ // if the user instructed "effective" then we need to selected
+ // greedy total coin value
return {
info,
value: info.value,
@@ -690,8 +599,8 @@ function rankDenominationForWithdrawals(
};
}
case TransactionAmountMode.Raw: {
- //if the user instructed "raw" then we need to selected
- //greedy total coin raw amount (without fee)
+ // if the user instructed "raw" then we need to selected
+ // greedy total coin raw amount (without fee)
return {
info,
value: Amounts.add(info.value, info.denomWithdraw).amount,
@@ -713,17 +622,15 @@ function rankDenominationForDeposit(
mode: TransactionAmountMode,
): SelectableElement[] {
const copyList = [...denoms];
- /**
- * Rank coins
- */
+ // Rank coins
copyList.sort((d1, d2) => {
// the best coin to use is
// 1.- the one that contrib more and pay less fee
// 2.- it takes more time before expires
- //different exchanges may have different wireFee
- //ranking should take the relative contribution in the exchange
- //which is (value - denomFee / fixedFee)
+ // different exchanges may have different wireFee
+ // ranking should take the relative contribution in the exchange
+ // which is (value - denomFee / fixedFee)
const rate1 = Amounts.isZero(d1.denomDeposit)
? Number.MIN_SAFE_INTEGER
: Amounts.divmod(d1.value, d1.denomDeposit).quotient;
@@ -742,8 +649,8 @@ function rankDenominationForDeposit(
return copyList.map((info) => {
switch (mode) {
case TransactionAmountMode.Effective: {
- //if the user instructed "effective" then we need to selected
- //greedy total coin value
+ // if the user instructed "effective" then we need to selected
+ // greedy total coin value
return {
info,
value: info.value,
@@ -751,8 +658,8 @@ function rankDenominationForDeposit(
};
}
case TransactionAmountMode.Raw: {
- //if the user instructed "raw" then we need to selected
- //greedy total coin raw amount (without fee)
+ // if the user instructed "raw" then we need to selected
+ // greedy total coin raw amount (without fee)
return {
info,
value: Amounts.sub(info.value, info.denomDeposit).amount,
@@ -765,9 +672,6 @@ function rankDenominationForDeposit(
/**
* Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
*/
function rankDenominationForRefresh(
denoms: CoinInfo[],
@@ -817,7 +721,7 @@ function selectGreedyCoins(
break iterateDenoms;
}
- //use Amounts.divmod instead of iterate
+ // use Amounts.divmod instead of iterate
const div = Amounts.divmod(left, denom.value);
const size = Math.min(div.quotient, denom.total);
if (size > 0) {
@@ -829,7 +733,7 @@ function selectGreedyCoins(
denom.total = denom.total - size;
}
- //go next denom
+ // go next denom
denomIdx++;
}
@@ -839,7 +743,7 @@ function selectGreedyCoins(
type AmountWithFee = { raw: AmountJson; effective: AmountJson };
type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
-export function getTotalEffectiveAndRawForDeposit(
+function getTotalEffectiveAndRawForDeposit(
list: { info: CoinInfo; size: number }[],
currency: string,
): AmountWithFee {
diff --git a/packages/taler-wallet-core/src/observable-wrappers.ts b/packages/taler-wallet-core/src/observable-wrappers.ts
index 717de41ca..e26fac638 100644
--- a/packages/taler-wallet-core/src/observable-wrappers.ts
+++ b/packages/taler-wallet-core/src/observable-wrappers.ts
@@ -23,6 +23,7 @@
*/
import { IDBDatabase } from "@gnu-taler/idb-bridge";
import {
+ getErrorDetailFromException,
ObservabilityContext,
ObservabilityEventType,
} from "@gnu-taler/taler-util";
@@ -161,6 +162,7 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
type: ObservabilityEventType.DbQueryFinishError,
name: "<unknown>",
location,
+ error: getErrorDetailFromException(e),
});
throw e;
}
@@ -193,6 +195,7 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
type: ObservabilityEventType.DbQueryFinishError,
name: options.label ?? "<unknown>",
location,
+ error: getErrorDetailFromException(e),
});
throw e;
}
@@ -224,6 +227,7 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
type: ObservabilityEventType.DbQueryFinishError,
name: opts.label ?? "<unknown>",
location,
+ error: getErrorDetailFromException(e),
});
throw e;
}
@@ -255,6 +259,7 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
type: ObservabilityEventType.DbQueryFinishError,
name: opts.label ?? "<unknown>",
location,
+ error: getErrorDetailFromException(e),
});
throw e;
}
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index ee154252f..2d993fea5 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -58,11 +58,13 @@ import {
Logger,
makeErrorDetail,
makePendingOperationFailedError,
+ makeTalerErrorDetail,
MerchantCoinRefundStatus,
MerchantContractTerms,
MerchantPayResponse,
MerchantUsingTemplateDetails,
NotificationType,
+ OrderShortInfo,
parsePayTemplateUri,
parsePayUri,
parseTalerUri,
@@ -71,6 +73,8 @@ import {
PreparePayTemplateRequest,
randomBytes,
RefreshReason,
+ RefundInfoShort,
+ RefundPaymentInfo,
SelectedProspectiveCoin,
SharePaymentResult,
StartRefundQueryForUriResponse,
@@ -84,6 +88,7 @@ import {
TalerPreciseTimestamp,
TalerProtocolViolationError,
TalerUriAction,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -95,17 +100,23 @@ import {
} from "@gnu-taler/taler-util";
import {
getHttpResponseErrorDetails,
+ HttpResponse,
readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
readUnexpectedResponseDetails,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPayCoins } from "./coinSelection.js";
+import {
+ PreviousPayCoins,
+ selectPayCoins,
+ selectPayCoinsInTx,
+} from "./coinSelection.js";
import {
constructTaskIdentifier,
PendingTaskType,
spendCoins,
+ TaskIdentifiers,
TaskIdStr,
TaskRunResult,
TaskRunResultType,
@@ -113,7 +124,7 @@ import {
TransactionContext,
TransitionResultType,
} from "./common.js";
-import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
+import { EddsaKeyPairStrings } from "./crypto/cryptoImplementation.js";
import {
CoinRecord,
DbCoinSelection,
@@ -125,13 +136,16 @@ import {
RefundItemRecord,
RefundItemStatus,
RefundReason,
+ timestampPreciseFromDb,
timestampPreciseToDb,
timestampProtocolFromDb,
timestampProtocolToDb,
+ WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletStoresV1,
} from "./db.js";
+import { getScopeForAllExchanges } from "./exchanges.js";
import { DbReadWriteTransaction, StoreNames } from "./query.js";
import {
calculateRefreshOutput,
@@ -140,6 +154,7 @@ import {
} from "./refresh.js";
import {
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
@@ -173,6 +188,130 @@ export class PayMerchantTransactionContext implements TransactionContext {
}
/**
+ * Function that updates the metadata of the transaction.
+ *
+ * Must be called each time the DB record for the transaction is updated.
+ */
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["purchases", "transactionsMeta"]>,
+ ): Promise<void> {
+ const purchaseRec = await tx.purchases.get(this.proposalId);
+ if (!purchaseRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ if (!purchaseRec.download) {
+ // Transaction is not reportable yet
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: purchaseRec.purchaseStatus,
+ timestamp: purchaseRec.timestamp,
+ currency: purchaseRec.download?.currency,
+ // FIXME!
+ exchanges: [],
+ });
+ }
+
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const proposalId = this.proposalId;
+ const purchaseRec = await tx.purchases.get(proposalId);
+ if (!purchaseRec) throw Error("not found");
+ const download = await expectProposalDownloadInTx(
+ this.wex,
+ tx,
+ purchaseRec,
+ );
+ const contractData = download.contractData;
+ const payOpId = TaskIdentifiers.forPay(purchaseRec);
+ const payRetryRec = await tx.operationRetries.get(payOpId);
+
+ const refundsInfo = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchaseRec.proposalId,
+ );
+
+ const zero = Amounts.zeroOfAmount(contractData.amount);
+
+ const info: OrderShortInfo = {
+ merchant: {
+ name: contractData.merchant.name,
+ address: contractData.merchant.address,
+ email: contractData.merchant.email,
+ jurisdiction: contractData.merchant.jurisdiction,
+ website: contractData.merchant.website,
+ },
+ orderId: contractData.orderId,
+ summary: contractData.summary,
+ summary_i18n: contractData.summaryI18n,
+ contractTermsHash: contractData.contractTermsHash,
+ };
+
+ if (contractData.fulfillmentUrl !== "") {
+ info.fulfillmentUrl = contractData.fulfillmentUrl;
+ }
+
+ const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
+ amountEffective: r.amountEffective,
+ amountRaw: r.amountRaw,
+ timestamp: TalerPreciseTimestamp.round(
+ timestampPreciseFromDb(r.timestampCreated),
+ ),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId: r.refundGroupId,
+ }),
+ }));
+
+ const timestamp = purchaseRec.timestampAccept;
+ if (!timestamp) {
+ return undefined;
+ }
+ if (!purchaseRec.payInfo) {
+ return undefined;
+ }
+
+ const txState = computePayMerchantTransactionState(purchaseRec);
+ return {
+ type: TransactionType.Payment,
+ txState,
+ scopes: await getScopeForAllExchanges(
+ tx,
+ !purchaseRec.payInfo.payCoinSelection
+ ? []
+ : purchaseRec.payInfo.payCoinSelection.coinPubs,
+ ),
+ txActions: computePayMerchantTransactionActions(purchaseRec),
+ amountRaw: Amounts.stringify(contractData.amount),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(zero)
+ : Amounts.stringify(purchaseRec.payInfo.totalPayCost),
+ totalRefundRaw: Amounts.stringify(zero), // FIXME!
+ totalRefundEffective: Amounts.stringify(zero), // FIXME!
+ refundPending:
+ purchaseRec.refundAmountAwaiting === undefined
+ ? undefined
+ : Amounts.stringify(purchaseRec.refundAmountAwaiting),
+ refunds,
+ posConfirmation: purchaseRec.posConfirmation,
+ timestamp: timestampPreciseFromDb(timestamp),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: purchaseRec.proposalId,
+ }),
+ proposalId: purchaseRec.proposalId,
+ abortReason: purchaseRec.abortReason,
+ info,
+ refundQueryActive:
+ purchaseRec.purchaseStatus === PurchaseStatus.PendingQueryingRefund,
+ ...(payRetryRec?.lastError ? { error: payRetryRec.lastError } : {}),
+ };
+ }
+
+ /**
* Transition a payment transition.
*/
async transition(
@@ -198,14 +337,14 @@ export class PayMerchantTransactionContext implements TransactionContext {
rec: PurchaseRecord,
tx: DbReadWriteTransaction<
typeof WalletStoresV1,
- ["purchases", ...StoreNameArray]
+ ["purchases", "transactionsMeta", ...StoreNameArray]
>,
) => Promise<TransitionResultType>,
): Promise<void> {
const ws = this.wex;
const extraStores = opts.extraStores ?? [];
const transitionInfo = await ws.db.runReadWriteTx(
- { storeNames: ["purchases", ...extraStores] },
+ { storeNames: ["purchases", "transactionsMeta", ...extraStores] },
async (tx) => {
const purchaseRec = await tx.purchases.get(this.proposalId);
if (!purchaseRec) {
@@ -216,12 +355,22 @@ export class PayMerchantTransactionContext implements TransactionContext {
switch (res) {
case TransitionResultType.Transition: {
await tx.purchases.put(purchaseRec);
+ await this.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchaseRec);
return {
oldTxState,
newTxState,
};
}
+ case TransitionResultType.Delete:
+ await tx.purchases.delete(this.proposalId);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
default:
return undefined;
}
@@ -233,13 +382,14 @@ export class PayMerchantTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex: ws, proposalId } = this;
await ws.db.runReadWriteTx(
- { storeNames: ["purchases", "tombstones"] },
+ { storeNames: ["purchases", "tombstones", "transactionsMeta"] },
async (tx) => {
let found = false;
const purchase = await tx.purchases.get(proposalId);
if (purchase) {
found = true;
await tx.purchases.delete(proposalId);
+ await this.updateTransactionMeta(tx);
}
if (found) {
await tx.tombstones.put({
@@ -254,7 +404,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
const { wex, proposalId, transactionId } = this;
wex.taskScheduler.stopShepherdTask(this.taskId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -266,6 +416,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return undefined;
}
await tx.purchases.put(purchase);
+ await this.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -273,18 +424,20 @@ export class PayMerchantTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, proposalId, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
- "refreshGroups",
- "refreshSessions",
- "denominations",
"coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
"operationRetries",
+ "purchases",
+ "refreshGroups",
+ "refreshSessions",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -299,6 +452,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
case PurchaseStatus.PendingPaying:
case PurchaseStatus.SuspendedPaying: {
+ purchase.abortReason = reason;
purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
if (purchase.payInfo && purchase.payInfo.payCoinSelection) {
const coinSel = purchase.payInfo.payCoinSelection;
@@ -341,6 +495,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return;
}
await tx.purchases.put(purchase);
+ await this.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -353,7 +508,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, proposalId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -365,6 +520,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
return undefined;
}
await tx.purchases.put(purchase);
+ await this.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -373,7 +529,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
wex.taskScheduler.startShepherdTask(this.taskId);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, proposalId, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
{
@@ -384,6 +540,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
"coinAvailability",
"coins",
"operationRetries",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -400,8 +557,10 @@ export class PayMerchantTransactionContext implements TransactionContext {
}
if (newState) {
purchase.purchaseStatus = newState;
+ purchase.failReason = reason;
await tx.purchases.put(purchase);
}
+ await this.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
@@ -414,6 +573,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
export class RefundTransactionContext implements TransactionContext {
public transactionId: TransactionIdStr;
public taskId: TaskIdStr | undefined = undefined;
+
constructor(
public wex: WalletExecutionContext,
public refundGroupId: string,
@@ -424,16 +584,91 @@ export class RefundTransactionContext implements TransactionContext {
});
}
+ /**
+ * Function that updates the metadata of the transaction.
+ *
+ * Must be called each time the DB record for the transaction is updated.
+ */
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["refundGroups", "transactionsMeta"]>,
+ ): Promise<void> {
+ const refundRec = await tx.refundGroups.get(this.refundGroupId);
+ if (!refundRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: refundRec.status,
+ timestamp: refundRec.timestampCreated,
+ currency: Amounts.currencyOf(refundRec.amountEffective),
+ // FIXME!
+ exchanges: [],
+ });
+ }
+
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const refundRecord = await tx.refundGroups.get(this.refundGroupId);
+ if (!refundRecord) {
+ throw Error("not found");
+ }
+ const maybeContractData = await lookupMaybeContractData(
+ tx,
+ refundRecord?.proposalId,
+ );
+
+ let paymentInfo: RefundPaymentInfo | undefined = undefined;
+
+ if (maybeContractData) {
+ paymentInfo = {
+ merchant: maybeContractData.merchant,
+ summary: maybeContractData.summary,
+ summary_i18n: maybeContractData.summaryI18n,
+ };
+ }
+ const purchaseRecord = await tx.purchases.get(refundRecord.proposalId);
+
+ const txState = computeRefundTransactionState(refundRecord);
+ return {
+ type: TransactionType.Refund,
+ scopes: await getScopeForAllExchanges(
+ tx,
+ !purchaseRecord || !purchaseRecord.payInfo?.payCoinSelection
+ ? []
+ : purchaseRecord.payInfo.payCoinSelection.coinPubs,
+ ),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(refundRecord.amountEffective))
+ : refundRecord.amountEffective,
+ amountRaw: refundRecord.amountRaw,
+ refundedTransactionId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: refundRecord.proposalId,
+ }),
+ timestamp: timestampPreciseFromDb(refundRecord.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refund,
+ refundGroupId: refundRecord.refundGroupId,
+ }),
+ txState,
+ txActions: [],
+ paymentInfo,
+ };
+ }
+
async deleteTransaction(): Promise<void> {
const { wex, refundGroupId, transactionId } = this;
await wex.db.runReadWriteTx(
- { storeNames: ["refundGroups", "tombstones"] },
+ { storeNames: ["refundGroups", "tombstones", "transactionsMeta"] },
async (tx) => {
const refundRecord = await tx.refundGroups.get(refundGroupId);
if (!refundRecord) {
return;
}
await tx.refundGroups.delete(refundGroupId);
+ await this.updateTransactionMeta(tx);
await tx.tombstones.put({ id: transactionId });
// FIXME: Also tombstone the refund items, so that they won't reappear.
},
@@ -457,6 +692,30 @@ export class RefundTransactionContext implements TransactionContext {
}
}
+async function lookupMaybeContractData(
+ tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>,
+ proposalId: string,
+): Promise<WalletContractData | undefined> {
+ let contractData: WalletContractData | undefined = undefined;
+ const purchaseTx = await tx.purchases.get(proposalId);
+ if (purchaseTx && purchaseTx.download) {
+ const download = purchaseTx.download;
+ const contractTermsRecord = await tx.contractTerms.get(
+ download.contractTermsHash,
+ );
+ if (!contractTermsRecord) {
+ return;
+ }
+ contractData = extractContractData(
+ contractTermsRecord?.contractTermsRaw,
+ download.contractTermsHash,
+ download.contractTermsMerchantSig,
+ );
+ }
+
+ return contractData;
+}
+
/**
* Compute the total cost of a payment to the customer.
*
@@ -472,44 +731,50 @@ export async function getTotalPaymentCost(
return wex.db.runReadOnlyTx(
{ storeNames: ["coins", "denominations"] },
async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.length; i++) {
- const denom = await tx.denominations.get([
- pcs[i].exchangeBaseUrl,
- pcs[i].denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
- );
- }
- const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
- const refreshCost = await getTotalRefreshCost(
- wex,
- tx,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- );
- costs.push(Amounts.parseOrThrow(pcs[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfCurrency(currency);
- return Amounts.sum([zero, ...costs]).amount;
+ return getTotalPaymentCostInTx(wex, tx, currency, pcs);
},
);
}
+export async function getTotalPaymentCostInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ currency: string,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denom = await tx.denominations.get([
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ DenominationRecord.toDenomInfo(denom),
+ amountLeft,
+ );
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
+ }
+ const zero = Amounts.zeroOfCurrency(currency);
+ return Amounts.sum([zero, ...costs]).amount;
+}
+
async function failProposalPermanently(
wex: WalletExecutionContext,
proposalId: string,
err: TalerErrorDetail,
): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -520,10 +785,11 @@ async function failProposalPermanently(
p.purchaseStatus = PurchaseStatus.FailedClaim;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
}
function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
@@ -533,13 +799,10 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
);
}
-/**
- * Return the proposal download data for a purchase, throw if not available.
- */
-export async function expectProposalDownload(
+export async function expectProposalDownloadInTx(
wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["contractTerms"]>,
p: PurchaseRecord,
- parentTx?: WalletDbReadOnlyTransaction<["contractTerms"]>,
): Promise<{
contractData: WalletContractData;
contractTermsRaw: any;
@@ -549,31 +812,35 @@ export async function expectProposalDownload(
}
const download = p.download;
- async function getFromTransaction(
- tx: Exclude<typeof parentTx, undefined>,
- ): Promise<ReturnType<typeof expectProposalDownload>> {
- const contractTerms = await tx.contractTerms.get(
- download.contractTermsHash,
- );
- if (!contractTerms) {
- throw Error("contract terms not found");
- }
- return {
- contractData: extractContractData(
- contractTerms.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- ),
- contractTermsRaw: contractTerms.contractTermsRaw,
- };
+ const contractTerms = await tx.contractTerms.get(download.contractTermsHash);
+ if (!contractTerms) {
+ throw Error("contract terms not found");
}
+ return {
+ contractData: extractContractData(
+ contractTerms.contractTermsRaw,
+ download.contractTermsHash,
+ download.contractTermsMerchantSig,
+ ),
+ contractTermsRaw: contractTerms.contractTermsRaw,
+ };
+}
- if (parentTx) {
- return getFromTransaction(parentTx);
- }
+/**
+ * Return the proposal download data for a purchase, throw if not available.
+ */
+export async function expectProposalDownload(
+ wex: WalletExecutionContext,
+ p: PurchaseRecord,
+): Promise<{
+ contractData: WalletContractData;
+ contractTermsRaw: any;
+}> {
return await wex.db.runReadOnlyTx(
{ storeNames: ["contractTerms"] },
- getFromTransaction,
+ async (tx) => {
+ return expectProposalDownloadInTx(wex, tx, p);
+ },
);
}
@@ -789,7 +1056,7 @@ async function processDownloadProposal(
logger.trace(`extracted contract data: ${j2s(contractData)}`);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases", "contractTerms"] },
+ { storeNames: ["purchases", "contractTerms", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -813,22 +1080,29 @@ async function processDownloadProposal(
fulfillmentUrl &&
(fulfillmentUrl.startsWith("http://") ||
fulfillmentUrl.startsWith("https://"));
- let otherPurchase: PurchaseRecord | undefined;
+ let repurchase: PurchaseRecord | undefined = undefined;
+ const otherPurchases =
+ await tx.purchases.indexes.byFulfillmentUrl.getAll(fulfillmentUrl);
if (isResourceFulfillmentUrl) {
- otherPurchase =
- await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
+ for (const otherPurchase of otherPurchases) {
+ if (
+ otherPurchase.purchaseStatus == PurchaseStatus.Done ||
+ otherPurchase.purchaseStatus == PurchaseStatus.PendingPaying ||
+ otherPurchase.purchaseStatus == PurchaseStatus.PendingPayingReplay
+ ) {
+ repurchase = otherPurchase;
+ break;
+ }
+ }
}
+
// FIXME: Adjust this to account for refunds, don't count as repurchase
// if original order is refunded.
- if (
- otherPurchase &&
- (otherPurchase.purchaseStatus == PurchaseStatus.Done ||
- otherPurchase.purchaseStatus == PurchaseStatus.PendingPaying ||
- otherPurchase.purchaseStatus == PurchaseStatus.PendingPayingReplay)
- ) {
+
+ if (repurchase) {
logger.warn("repurchase detected");
p.purchaseStatus = PurchaseStatus.DoneRepurchaseDetected;
- p.repurchaseProposalId = otherPurchase.proposalId;
+ p.repurchaseProposalId = repurchase.proposalId;
await tx.purchases.put(p);
} else {
p.purchaseStatus = p.shared
@@ -836,6 +1110,7 @@ async function processDownloadProposal(
: PurchaseStatus.DialogProposed;
await tx.purchases.put(p);
}
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(p);
return {
oldTxState,
@@ -903,8 +1178,12 @@ async function createOrReusePurchase(
if (paid) {
// if this transaction was shared and the order is paid then it
// means that another wallet already paid the proposal
+ const ctx = new PayMerchantTransactionContext(
+ wex,
+ oldProposal.proposalId,
+ );
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(oldProposal.proposalId);
if (!p) {
@@ -915,6 +1194,7 @@ async function createOrReusePurchase(
p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
@@ -929,7 +1209,7 @@ async function createOrReusePurchase(
return oldProposal.proposalId;
}
- let noncePair: EddsaKeypair;
+ let noncePair: EddsaKeyPairStrings;
let shared = false;
if (noncePriv) {
shared = true;
@@ -944,6 +1224,10 @@ async function createOrReusePurchase(
const { priv, pub } = noncePair;
const proposalId = encodeCrock(getRandomBytes(32));
+ logger.info(
+ `created new proposal for ${orderId} at ${merchantBaseUrl} session ${sessionId}`,
+ );
+
const proposalRecord: PurchaseRecord = {
download: undefined,
noncePriv: priv,
@@ -969,10 +1253,13 @@ async function createOrReusePurchase(
shared: shared,
};
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
await tx.purchases.put(proposalRecord);
+ await ctx.updateTransactionMeta(tx);
const oldTxState: TransactionState = {
major: TransactionMajorState.None,
};
@@ -984,11 +1271,7 @@ async function createOrReusePurchase(
},
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return proposalId;
}
@@ -998,13 +1281,10 @@ async function storeFirstPaySuccess(
sessionId: string | undefined,
payResponse: MerchantPayResponse,
): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["contractTerms", "purchases"] },
+ { storeNames: ["contractTerms", "purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -1046,7 +1326,7 @@ async function storeFirstPaySuccess(
if (protoAr) {
const ar = Duration.fromTalerProtocolDuration(protoAr);
logger.info("auto_refund present");
- purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
+ purchase.purchaseStatus = PurchaseStatus.FinalizingQueryingAutoRefund;
purchase.autoRefundDeadline = timestampProtocolToDb(
AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
@@ -1054,6 +1334,7 @@ async function storeFirstPaySuccess(
);
}
await tx.purchases.put(purchase);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return {
oldTxState,
@@ -1061,7 +1342,7 @@ async function storeFirstPaySuccess(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
}
async function storePayReplaySuccess(
@@ -1069,12 +1350,9 @@ async function storePayReplaySuccess(
proposalId: string,
sessionId: string | undefined,
): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -1095,11 +1373,12 @@ async function storePayReplaySuccess(
}
purchase.lastSessionId = sessionId;
await tx.purchases.put(purchase);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(purchase);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
}
/**
@@ -1117,6 +1396,8 @@ async function handleInsufficientFunds(
): Promise<void> {
logger.trace("handling insufficient funds, trying to re-select coins");
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
const proposal = await wex.db.runReadOnlyTx(
{ storeNames: ["purchases"] },
async (tx) => {
@@ -1148,8 +1429,6 @@ async function handleInsufficientFunds(
throw new TalerProtocolViolationError();
}
- const { contractData } = await expectProposalDownload(wex, proposal);
-
const prevPayCoins: PreviousPayCoins = [];
const payInfo = proposal.payInfo;
@@ -1162,55 +1441,22 @@ async function handleInsufficientFunds(
return;
}
- await wex.db.runReadOnlyTx(
- { storeNames: ["coins", "denominations"] },
- async (tx) => {
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- const coinPub = payCoinSelection.coinPubs[i];
- const contrib = payCoinSelection.coinContributions[i];
- prevPayCoins.push({
- coinPub,
- contribution: Amounts.parseOrThrow(contrib),
- });
- }
- },
- );
-
- const res = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins,
- requiredMinimumAge: contractData.minimumAge,
- });
-
- switch (res.type) {
- case "failure":
- logger.trace("insufficient funds for coin re-selection");
- return;
- case "prospective":
- return;
- case "success":
- break;
- default:
- assertUnreachable(res);
- }
-
- logger.trace("re-selected coins");
+ // FIXME: Above code should go into the transaction.
await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
- "coins",
"coinAvailability",
+ "coinHistory",
+ "coins",
+ "contractTerms",
"denominations",
+ "exchangeDetails",
+ "exchanges",
+ "purchases",
"refreshGroups",
"refreshSessions",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1222,6 +1468,46 @@ async function handleInsufficientFunds(
if (!payInfo) {
return;
}
+
+ const { contractData } = await expectProposalDownloadInTx(
+ wex,
+ tx,
+ proposal,
+ );
+
+ for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
+ const contrib = payCoinSelection.coinContributions[i];
+ prevPayCoins.push({
+ coinPub,
+ contribution: Amounts.parseOrThrow(contrib),
+ });
+ }
+
+ const res = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins,
+ requiredMinimumAge: contractData.minimumAge,
+ });
+
+ switch (res.type) {
+ case "failure":
+ logger.trace("insufficient funds for coin re-selection");
+ return;
+ case "prospective":
+ return;
+ case "success":
+ break;
+ default:
+ assertUnreachable(res);
+ }
+
// Convert to DB format
payInfo.payCoinSelection = {
coinContributions: res.coinSel.coins.map((x) => x.contribution),
@@ -1229,12 +1515,9 @@ async function handleInsufficientFunds(
};
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
await spendCoins(wex, tx, {
- // allocationId: `txn:proposal:${p.proposalId}`,
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: proposalId,
- }),
+ transactionId: ctx.transactionId,
coinPubs: payInfo.payCoinSelection.coinPubs,
contributions: payInfo.payCoinSelection.coinContributions.map((x) =>
Amounts.parseOrThrow(x),
@@ -1382,15 +1665,15 @@ async function checkPaymentByProposalId(
}
if (
- purchase.purchaseStatus === PurchaseStatus.Done &&
- purchase.lastSessionId !== sessionId
+ purchase.purchaseStatus === PurchaseStatus.Done ||
+ purchase.purchaseStatus === PurchaseStatus.PendingPayingReplay
) {
logger.trace(
"automatically re-submitting payment with different session ID",
);
logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -1400,6 +1683,7 @@ async function checkPaymentByProposalId(
p.lastSessionId = sessionId;
p.purchaseStatus = PurchaseStatus.PendingPayingReplay;
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(p);
return { oldTxState, newTxState };
},
@@ -1441,10 +1725,7 @@ async function checkPaymentByProposalId(
talerUri,
};
} else {
- const paid =
- purchase.purchaseStatus === PurchaseStatus.Done ||
- purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund ||
- purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund;
+ const paid = isPurchasePaid(purchase);
const download = await expectProposalDownload(wex, purchase);
return {
status: PreparePayResultType.AlreadyConfirmed,
@@ -1463,6 +1744,15 @@ async function checkPaymentByProposalId(
}
}
+function isPurchasePaid(purchase: PurchaseRecord): boolean {
+ return (
+ purchase.purchaseStatus === PurchaseStatus.Done ||
+ purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund ||
+ purchase.purchaseStatus === PurchaseStatus.FinalizingQueryingAutoRefund ||
+ purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund
+ );
+}
+
export async function getContractTermsDetails(
wex: WalletExecutionContext,
proposalId: string,
@@ -1626,7 +1916,13 @@ export async function checkPayForTemplate(
const cfg = await merchantApi.getConfig();
if (cfg.type === "fail") {
- throw TalerError.fromUncheckedDetail(cfg.detail);
+ if (cfg.detail) {
+ throw TalerError.fromUncheckedDetail(cfg.detail);
+ } else {
+ throw TalerError.fromException(
+ new Error("failed to get merchant remote config"),
+ );
+ }
}
// FIXME: Put body.currencies *and* body.currency in the set of
@@ -1881,6 +2177,7 @@ export async function confirmPay(
throw Error("expected payment transaction ID");
}
const proposalId = parsedTx.proposalId;
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
logger.trace(
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
);
@@ -1901,7 +2198,7 @@ export async function confirmPay(
}
const existingPurchase = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (
@@ -1915,6 +2212,7 @@ export async function confirmPay(
purchase.purchaseStatus = PurchaseStatus.PendingPayingReplay;
}
await tx.purchases.put(purchase);
+ await ctx.updateTransactionMeta(tx);
}
return purchase;
},
@@ -1936,44 +2234,6 @@ export async function confirmPay(
const currency = Amounts.currencyOf(contractData.amount);
- const selectCoinsResult = await selectPayCoins(wex, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- },
- restrictWireMethod: contractData.wireMethod,
- contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
- depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimumAge,
- forcedSelection: forcedCoinSel,
- });
-
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
-
- switch (selectCoinsResult.type) {
- case "failure": {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
- logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
- case "prospective": {
- coins = selectCoinsResult.result.prospectiveCoins;
- break;
- }
- case "success":
- coins = selectCoinsResult.coinSel.coins;
- break;
- default:
- assertUnreachable(selectCoinsResult);
- }
-
- logger.trace("coin selection result", selectCoinsResult);
-
- const payCostInfo = await getTotalPaymentCost(wex, currency, coins);
-
let sessionId: string | undefined;
if (sessionIdOverride) {
sessionId = sessionIdOverride;
@@ -1988,12 +2248,16 @@ export async function confirmPay(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
+ "coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
+ "exchangeDetails",
+ "exchanges",
+ "purchases",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -2001,6 +2265,50 @@ export async function confirmPay(
if (!p) {
return;
}
+
+ const selectCoinsResult = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ requiredMinimumAge: contractData.minimumAge,
+ forcedSelection: forcedCoinSel,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (selectCoinsResult.type) {
+ case "failure": {
+ // Should not happen, since checkPay should be called first
+ // FIXME: Actually, this should be handled gracefully,
+ // and the status should be stored in the DB.
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+ case "prospective": {
+ coins = selectCoinsResult.result.prospectiveCoins;
+ break;
+ }
+ case "success":
+ coins = selectCoinsResult.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
+ }
+
+ logger.trace("coin selection result", selectCoinsResult);
+
+ const payCostInfo = await getTotalPaymentCostInTx(
+ wex,
+ tx,
+ currency,
+ coins,
+ );
+
const oldTxState = computePayMerchantTransactionState(p);
switch (p.purchaseStatus) {
case PurchaseStatus.DialogShared:
@@ -2021,14 +2329,11 @@ export async function confirmPay(
p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
p.purchaseStatus = PurchaseStatus.PendingPaying;
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
if (p.payInfo.payCoinSelection) {
const sel = p.payInfo.payCoinSelection;
await spendCoins(wex, tx, {
- //`txn:proposal:${p.proposalId}`
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: proposalId,
- }),
+ transactionId: transactionId as TransactionIdStr,
coinPubs: sel.coinPubs,
contributions: sel.coinContributions.map((x) =>
Amounts.parseOrThrow(x),
@@ -2036,7 +2341,6 @@ export async function confirmPay(
refreshReason: RefreshReason.PayMerchant,
});
}
-
break;
case PurchaseStatus.Done:
case PurchaseStatus.PendingPaying:
@@ -2049,13 +2353,7 @@ export async function confirmPay(
);
notifyTransition(wex, transactionId, transitionInfo);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
-
- const ctx = new PayMerchantTransactionContext(wex, proposalId);
-
+
// In case we're sharing the payment and we're long-polling
wex.taskScheduler.stopShepherdTask(ctx.taskId);
@@ -2086,6 +2384,10 @@ export async function processPurchase(
};
}
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
switch (purchase.purchaseStatus) {
case PurchaseStatus.PendingDownloadingProposal:
return processDownloadProposal(wex, proposalId);
@@ -2093,8 +2395,8 @@ export async function processPurchase(
case PurchaseStatus.PendingPayingReplay:
return processPurchasePay(wex, proposalId);
case PurchaseStatus.PendingQueryingRefund:
- case PurchaseStatus.FinalizingQueryingAutoRefund:
return processPurchaseQueryRefund(wex, purchase);
+ case PurchaseStatus.FinalizingQueryingAutoRefund:
case PurchaseStatus.PendingQueryingAutoRefund:
return processPurchaseAutoRefund(wex, purchase);
case PurchaseStatus.AbortingWithRefund:
@@ -2177,7 +2479,7 @@ async function processPurchasePay(
if (paid) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2188,6 +2490,7 @@ async function processPurchasePay(
p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
@@ -2251,12 +2554,14 @@ async function processPurchasePay(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
+ "coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
+ "purchases",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -2282,13 +2587,9 @@ async function processPurchasePay(
p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16));
p.purchaseStatus = PurchaseStatus.PendingPaying;
await tx.purchases.put(p);
-
+ await ctx.updateTransactionMeta(tx);
await spendCoins(wex, tx, {
- //`txn:proposal:${p.proposalId}`
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: proposalId,
- }),
+ transactionId: ctx.transactionId,
coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
contributions: selectCoinsResult.coinSel.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
@@ -2378,6 +2679,16 @@ async function processPurchasePay(
}
}
+ if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
+ logger.warn(`pay transaction aborted, merchant has KYC problems`);
+ await ctx.abortTransaction(
+ makeTalerErrorDetail(TalerErrorCode.WALLET_PAY_MERCHANT_KYC_MISSING, {
+ exchangeResponse: await resp.json(),
+ }),
+ );
+ return TaskRunResult.progress();
+ }
+
if (resp.status >= 400 && resp.status <= 499) {
logger.trace("got generic 4xx from merchant");
const err = await readTalerErrorResponse(resp);
@@ -2447,12 +2758,9 @@ export async function refuseProposal(
wex: WalletExecutionContext,
proposalId: string,
): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const proposal = await tx.purchases.get(proposalId);
if (!proposal) {
@@ -2469,11 +2777,12 @@ export async function refuseProposal(
proposal.purchaseStatus = PurchaseStatus.AbortedProposalRefused;
const newTxState = computePayMerchantTransactionState(proposal);
await tx.purchases.put(proposal);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
}
const transitionSuspend: {
@@ -2778,13 +3087,30 @@ export async function sharePayment(
merchantBaseUrl: string,
orderId: string,
): Promise<SharePaymentResult> {
- const result = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ // First, translate the order ID into a proposal ID
+ const proposalId = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["purchases"],
+ },
async (tx) => {
const p = await tx.purchases.indexes.byUrlAndOrderId.get([
merchantBaseUrl,
orderId,
]);
+ return p?.proposalId;
+ },
+ );
+
+ if (!proposalId) {
+ throw Error(`no proposal found for order id ${orderId}`);
+ }
+
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
+ const result = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases", "transactionsMeta"] },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
if (!p) {
logger.warn("purchase does not exist anymore");
return undefined;
@@ -2803,6 +3129,8 @@ export async function sharePayment(
await tx.purchases.put(p);
}
+ await ctx.updateTransactionMeta(tx);
+
const newTxState = computePayMerchantTransactionState(p);
return {
@@ -2822,8 +3150,6 @@ export async function sharePayment(
throw Error("This purchase can't be shared");
}
- const ctx = new PayMerchantTransactionContext(wex, result.proposalId);
-
notifyTransition(wex, ctx.transactionId, result.transitionInfo);
// schedule a task to watch for the status
@@ -2851,14 +3177,25 @@ async function checkIfOrderIsAlreadyPaid(
);
requestUrl.searchParams.set("h_contract", contract.contractTermsHash);
+ let resp: HttpResponse;
+
if (doLongPolling) {
- requestUrl.searchParams.set("timeout_ms", "30000");
+ resp = await wex.ws.runLongpollQueueing(
+ wex,
+ requestUrl.hostname,
+ async (timeoutMs) => {
+ requestUrl.searchParams.set("timeout_ms", `${timeoutMs}`);
+ return await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
+ } else {
+ resp = await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
}
- const resp = await wex.http.fetch(requestUrl.href, {
- cancellationToken: wex.cancellationToken,
- });
-
if (
resp.status === HttpStatusCode.Ok ||
resp.status === HttpStatusCode.Accepted ||
@@ -2879,11 +3216,12 @@ async function processPurchaseDialogShared(
const proposalId = purchase.proposalId;
logger.trace(`processing dialog-shared for proposal ${proposalId}`);
const download = await expectProposalDownload(wex, purchase);
-
if (purchase.purchaseStatus !== PurchaseStatus.DialogShared) {
return TaskRunResult.finished();
}
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
const paid = await checkIfOrderIsAlreadyPaid(
wex,
download.contractData,
@@ -2891,7 +3229,7 @@ async function processPurchaseDialogShared(
);
if (paid) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2902,6 +3240,7 @@ async function processPurchaseDialogShared(
p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
@@ -2921,12 +3260,9 @@ async function processPurchaseAutoRefund(
purchase: PurchaseRecord,
): Promise<TaskRunResult> {
const proposalId = purchase.proposalId;
- logger.trace(`processing auto-refund for proposal ${proposalId}`);
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ logger.trace(`processing auto-refund for proposal ${proposalId}`);
const download = await expectProposalDownload(wex, purchase);
@@ -2950,20 +3286,22 @@ async function processPurchaseAutoRefund(
cur.status === RefundGroupStatus.Done ||
cur.status === RefundGroupStatus.Pending
) {
- return Amounts.add(prev, cur.amountEffective).amount;
+ return Amounts.add(prev, cur.amountRaw).amount;
}
return prev;
}, Amounts.zeroOfAmount(am));
},
);
- const refundedIsLessThanPrice =
- Amounts.cmp(download.contractData.amount, totalKnownRefund) === +1;
- const nothingMoreToRefund = !refundedIsLessThanPrice;
+ const fullyRefunded =
+ Amounts.cmp(download.contractData.amount, totalKnownRefund) <= 0;
- if (noAutoRefundOrExpired || nothingMoreToRefund) {
+ // We stop with the auto-refund state when the auto-refund period
+ // is over or the product is already fully refunded.
+
+ if (noAutoRefundOrExpired || fullyRefunded) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2982,10 +3320,11 @@ async function processPurchaseAutoRefund(
p.refundAmountAwaiting = undefined;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.finished();
}
@@ -2998,12 +3337,18 @@ async function processPurchaseAutoRefund(
download.contractData.contractTermsHash,
);
- requestUrl.searchParams.set("timeout_ms", "10000");
requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund));
- const resp = await wex.http.fetch(requestUrl.href, {
- cancellationToken: wex.cancellationToken,
- });
+ const resp = await wex.ws.runLongpollQueueing(
+ wex,
+ requestUrl.hostname,
+ async (timeoutMs) => {
+ requestUrl.searchParams.set("timeout_ms", `${timeoutMs}`);
+ return await wex.http.fetch(requestUrl.href, {
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
// FIXME: Check other status codes!
@@ -3014,7 +3359,7 @@ async function processPurchaseAutoRefund(
if (orderStatus.refund_pending) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -3032,10 +3377,11 @@ async function processPurchaseAutoRefund(
p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
}
@@ -3161,14 +3507,11 @@ async function processPurchaseQueryRefund(
codecForMerchantOrderStatusPaid(),
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
if (!orderStatus.refund_pending) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -3183,10 +3526,11 @@ async function processPurchaseQueryRefund(
p.refundAmountAwaiting = undefined;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
} else {
const refundAwaiting = Amounts.sub(
@@ -3195,7 +3539,7 @@ async function processPurchaseQueryRefund(
).amount;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -3210,10 +3554,11 @@ async function processPurchaseQueryRefund(
p.purchaseStatus = PurchaseStatus.PendingAcceptRefund;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
}
}
@@ -3294,7 +3639,7 @@ export async function startQueryRefund(
): Promise<void> {
const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["purchases"] },
+ { storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -3308,6 +3653,7 @@ export async function startQueryRefund(
p.purchaseStatus = PurchaseStatus.PendingQueryingRefund;
const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
@@ -3375,10 +3721,7 @@ async function storeRefunds(
): Promise<TaskRunResult> {
logger.info(`storing refunds: ${j2s(refunds)}`);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: purchase.proposalId,
- });
+ const ctx = new PayMerchantTransactionContext(wex, purchase.proposalId);
const newRefundGroupId = encodeCrock(randomBytes(32));
const now = TalerPreciseTimestamp.now();
@@ -3389,16 +3732,18 @@ async function storeRefunds(
const result = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
"coins",
"denominations",
- "purchases",
- "refundItems",
- "refundGroups",
"denominations",
- "coins",
- "coinAvailability",
+ "purchases",
"refreshGroups",
"refreshSessions",
+ "refundGroups",
+ "refundItems",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -3499,7 +3844,12 @@ async function storeRefunds(
newGroup.amountRaw = Amounts.stringify(
Amounts.sumOrZero(currency, amountsRaw).amount,
);
+ const refundCtx = new RefundTransactionContext(
+ wex,
+ newGroup.refundGroupId,
+ );
await tx.refundGroups.put(newGroup);
+ await refundCtx.updateTransactionMeta(tx);
}
const refundGroups = await tx.refundGroups.indexes.byProposalId.getAll(
@@ -3507,6 +3857,10 @@ async function storeRefunds(
);
for (const refundGroup of refundGroups) {
+ const refundCtx = new RefundTransactionContext(
+ wex,
+ refundGroup.refundGroupId,
+ );
switch (refundGroup.status) {
case RefundGroupStatus.Aborted:
case RefundGroupStatus.Expired:
@@ -3539,6 +3893,7 @@ async function storeRefunds(
refundGroup.status = RefundGroupStatus.Failed;
}
await tx.refundGroups.put(refundGroup);
+ await refundCtx.updateTransactionMeta(tx);
const refreshCoins = await computeRefreshRequest(wex, tx, items);
await createRefreshGroup(
wex,
@@ -3578,6 +3933,7 @@ async function storeRefunds(
myPurchase.refundAmountAwaiting = undefined;
}
await tx.purchases.put(myPurchase);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePayMerchantTransactionState(myPurchase);
return {
@@ -3594,7 +3950,7 @@ async function storeRefunds(
return TaskRunResult.finished();
}
- notifyTransition(wex, transactionId, result.transitionInfo);
+ notifyTransition(wex, ctx.transactionId, result.transitionInfo);
if (result.numPendingItemsTotal > 0) {
return TaskRunResult.backoff();
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index a1729ced7..b9b637c57 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -31,7 +31,11 @@ import {
codecOptional,
} from "@gnu-taler/taler-util";
import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
-import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
+import {
+ DbPeerPushPaymentCoinSelection,
+ ReserveRecord,
+ WalletDbReadOnlyTransaction,
+} from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -67,6 +71,7 @@ export async function queryCoinInfosForSelection(
denomSig: coin.denomSig,
ageCommitmentProof: coin.ageCommitmentProof,
contribution: csel.contributions[i],
+ feeDeposit: denom.feeDeposit,
});
}
},
@@ -74,6 +79,38 @@ export async function queryCoinInfosForSelection(
return infos;
}
+export async function getTotalPeerPaymentCostInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ pcs: SelectedProspectiveCoin[],
+): Promise<AmountJson> {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ );
+ if (!denomInfo) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(denomInfo.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ denomInfo,
+ amountLeft,
+ );
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
+ }
+ const zero = Amounts.zeroOfAmount(pcs[0].contribution);
+ return Amounts.sum([zero, ...costs]).amount;
+}
+
export async function getTotalPeerPaymentCost(
wex: WalletExecutionContext,
pcs: SelectedProspectiveCoin[],
@@ -81,34 +118,7 @@ export async function getTotalPeerPaymentCost(
return wex.db.runReadOnlyTx(
{ storeNames: ["coins", "denominations"] },
async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.length; i++) {
- const denomInfo = await getDenomInfo(
- wex,
- tx,
- pcs[i].exchangeBaseUrl,
- pcs[i].denomPubHash,
- );
- if (!denomInfo) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
- );
- }
- const amountLeft = Amounts.sub(
- denomInfo.value,
- pcs[i].contribution,
- ).amount;
- const refreshCost = await getTotalRefreshCost(
- wex,
- tx,
- denomInfo,
- amountLeft,
- );
- costs.push(Amounts.parseOrThrow(pcs[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfAmount(pcs[0].contribution);
- return Amounts.sum([zero, ...costs]).amount;
+ return getTotalPeerPaymentCostInTx(wex, tx, pcs);
},
);
}
@@ -143,7 +153,10 @@ export async function getMergeReserveInfo(
checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
if (ex.currentMergeReserveRowId != null) {
const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
- checkDbInvariant(!!reserve, `reserver ${ex.currentMergeReserveRowId} missing in db`);
+ checkDbInvariant(
+ !!reserve,
+ `reserver ${ex.currentMergeReserveRowId} missing in db`,
+ );
return reserve;
}
const reserve: ReserveRecord = {
@@ -151,7 +164,10 @@ export async function getMergeReserveInfo(
reservePub: newReservePair.pub,
};
const insertResp = await tx.reserves.put(reserve);
- checkDbInvariant(typeof insertResp.key === "number", `reserve key is not a number`);
+ checkDbInvariant(
+ typeof insertResp.key === "number",
+ `reserve key is not a number`,
+ );
reserve.rowId = insertResp.key;
ex.currentMergeReserveRowId = reserve.rowId;
await tx.exchanges.put(ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index b7fb13da3..52ac2ff1b 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -14,6 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * Imports.
+ */
import {
AbsoluteTime,
Amounts,
@@ -21,6 +24,7 @@ import {
CheckPeerPullCreditResponse,
ContractTermsUtil,
ExchangeReservePurseRequest,
+ ExchangeWalletKycStatus,
HttpStatusCode,
InitiatePeerPullCreditRequest,
InitiatePeerPullCreditResponse,
@@ -28,9 +32,11 @@ import {
NotificationType,
PeerContractTerms,
TalerErrorCode,
+ TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TalerUriAction,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -38,46 +44,65 @@ import {
TransactionState,
TransactionType,
WalletAccountMergeFlags,
- WalletKycUuid,
assertUnreachable,
checkDbInvariant,
+ codecForAccountKycStatus,
codecForAny,
- codecForWalletKycUuid,
+ codecForLegitimizationNeededResponse,
encodeCrock,
getRandomBytes,
j2s,
- makeErrorDetail,
+ stringifyPayPullUri,
stringifyTalerUri,
talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ readResponseJsonOrThrow,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
TaskIdStr,
+ TaskIdentifiers,
TaskRunResult,
- TaskRunResultType,
TombstoneTag,
TransactionContext,
+ TransitionResult,
+ TransitionResultType,
constructTaskIdentifier,
+ genericWaitForStateVal,
+ requireExchangeTosAcceptedOrThrow,
+ runWithClientCancellation,
} from "./common.js";
import {
- KycPendingInfo,
- KycUserType,
+ OperationRetryRecord,
PeerPullCreditRecord,
PeerPullPaymentCreditStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletDbStoresArr,
+ WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
timestampOptionalPreciseFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
-import { fetchFreshExchange } from "./exchanges.js";
+import {
+ BalanceThresholdCheckResult,
+ checkIncomingAmountLegalUnderKycBalanceThreshold,
+ fetchFreshExchange,
+ getScopeForAllExchanges,
+ handleStartExchangeWalletKyc,
+} from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
+ TransitionInfo,
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
} from "./transactions.js";
import { WalletExecutionContext } from "./wallet.js";
@@ -107,10 +132,251 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
});
}
+ /**
+ * Transition a peer-pull-credit transaction.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transition<StoreNameArray extends WalletDbStoresArr = []>(
+ opts: { extraStores?: StoreNameArray; transactionLabel?: string },
+ f: (
+ rec: PeerPullCreditRecord | undefined,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "peerPullCredit",
+ "transactionsMeta",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ...StoreNameArray,
+ ]
+ >,
+ ) => Promise<TransitionResult<PeerPullCreditRecord>>,
+ ): Promise<TransitionInfo | undefined> {
+ const baseStores = [
+ "peerPullCredit" as const,
+ "transactionsMeta" as const,
+ "operationRetries" as const,
+ "exchanges" as const,
+ "exchangeDetails" as const,
+ ];
+ const stores = opts.extraStores
+ ? [...baseStores, ...opts.extraStores]
+ : baseStores;
+
+ let errorThrown: Error | undefined;
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: stores },
+ async (tx) => {
+ const rec = await tx.peerPullCredit.get(this.pursePub);
+ let oldTxState: TransactionState;
+ if (rec) {
+ oldTxState = computePeerPullCreditTransactionState(rec);
+ } else {
+ oldTxState = {
+ major: TransactionMajorState.None,
+ };
+ }
+ let res: TransitionResult<PeerPullCreditRecord> | undefined;
+ try {
+ res = await f(rec, tx);
+ } catch (error) {
+ if (error instanceof Error) {
+ errorThrown = error;
+ }
+ return undefined;
+ }
+
+ switch (res.type) {
+ case TransitionResultType.Transition: {
+ await tx.peerPullCredit.put(res.rec);
+ await this.updateTransactionMeta(tx);
+ const newTxState = computePeerPullCreditTransactionState(res.rec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ case TransitionResultType.Delete:
+ await tx.peerPullCredit.delete(this.pursePub);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ default:
+ return undefined;
+ }
+ },
+ );
+ if (errorThrown) {
+ throw errorThrown;
+ }
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ return transitionInfo;
+ }
+
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["peerPullCredit", "transactionsMeta"]>,
+ ): Promise<void> {
+ const ppcRec = await tx.peerPullCredit.get(this.pursePub);
+ if (!ppcRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: ppcRec.status,
+ timestamp: ppcRec.mergeTimestamp,
+ currency: Amounts.currencyOf(ppcRec.amount),
+ exchanges: [ppcRec.exchangeBaseUrl],
+ });
+ }
+
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const pullCredit = await tx.peerPullCredit.get(this.pursePub);
+ if (!pullCredit) {
+ return undefined;
+ }
+ const ct = await tx.contractTerms.get(pullCredit.contractTermsHash);
+ checkDbInvariant(!!ct, `no contract terms for p2p push ${this.pursePub}`);
+
+ const peerContractTerms = ct.contractTermsRaw;
+
+ let wsr: WithdrawalGroupRecord | undefined = undefined;
+ let wsrOrt: OperationRetryRecord | undefined = undefined;
+ if (pullCredit.withdrawalGroupId) {
+ wsr = await tx.withdrawalGroups.get(pullCredit.withdrawalGroupId);
+ if (wsr) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wsr);
+ wsrOrt = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pullCreditOpId =
+ TaskIdentifiers.forPeerPullPaymentInitiation(pullCredit);
+ let pullCreditOrt = await tx.operationRetries.get(pullCreditOpId);
+
+ let kycUrl: string | undefined = undefined;
+ if (pullCredit.kycPaytoHash) {
+ kycUrl = new URL(
+ `kyc-spa/${pullCredit.kycPaytoHash}`,
+ pullCredit.exchangeBaseUrl,
+ ).href;
+ }
+
+ if (wsr) {
+ if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
+ throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
+ }
+ /**
+ * FIXME: this should be handled in the withdrawal process.
+ * PeerPull withdrawal fails until reserve have funds but it is not
+ * an error from the user perspective.
+ */
+ const silentWithdrawalErrorForInvoice =
+ wsrOrt?.lastError &&
+ wsrOrt.lastError.code ===
+ TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
+ Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
+ return (
+ e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ e.httpStatusCode === 409
+ );
+ });
+ const txState = computePeerPullCreditTransactionState(pullCredit);
+ checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
+ return {
+ type: TransactionType.PeerPullCredit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pullCredit.exchangeBaseUrl]),
+ txActions: computePeerPullCreditTransactionActions(pullCredit),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount))
+ : Amounts.stringify(wsr.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wsr.instructedAmount),
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ talerUri: stringifyPayPullUri({
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ contractPriv: wsr.wgInfo.contractPriv,
+ }),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullCredit.pursePub,
+ }),
+ abortReason: pullCredit.abortReason,
+ failReason: pullCredit.failReason,
+ // FIXME: Is this the KYC URL of the withdrawal group?!
+ kycUrl: kycUrl,
+ ...(wsrOrt?.lastError
+ ? {
+ error: silentWithdrawalErrorForInvoice
+ ? undefined
+ : wsrOrt.lastError,
+ }
+ : {}),
+ };
+ }
+
+ const txState = computePeerPullCreditTransactionState(pullCredit);
+ return {
+ type: TransactionType.PeerPullCredit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pullCredit.exchangeBaseUrl]),
+ txActions: computePeerPullCreditTransactionActions(pullCredit),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
+ : Amounts.stringify(pullCredit.estimatedAmountEffective),
+ amountRaw: Amounts.stringify(peerContractTerms.amount),
+ exchangeBaseUrl: pullCredit.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ talerUri: stringifyPayPullUri({
+ exchangeBaseUrl: pullCredit.exchangeBaseUrl,
+ contractPriv: pullCredit.contractPriv,
+ }),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullCredit,
+ pursePub: pullCredit.pursePub,
+ }),
+ kycUrl,
+ kycAccessToken: pullCredit.kycAccessToken,
+ kycPaytoHash: pullCredit.kycPaytoHash,
+ abortReason: pullCredit.abortReason,
+ failReason: pullCredit.failReason,
+ ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
+ };
+ }
+
async deleteTransaction(): Promise<void> {
const { wex: ws, pursePub } = this;
await ws.db.runReadWriteTx(
- { storeNames: ["withdrawalGroups", "peerPullCredit", "tombstones"] },
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "peerPullCredit",
+ "tombstones",
+ "transactionsMeta",
+ ],
+ },
async (tx) => {
const pullIni = await tx.peerPullCredit.get(pursePub);
if (!pullIni) {
@@ -128,6 +394,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
}
}
await tx.peerPullCredit.delete(pursePub);
+ await this.updateTransactionMeta(tx);
await tx.tombstones.put({
id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
});
@@ -140,7 +407,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -165,15 +432,23 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
newStatus =
PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
break;
- case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired;
+ break;
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycInit;
+ break;
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
case PeerPullPaymentCreditStatus.SuspendedReady:
case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Done:
case PeerPullPaymentCreditStatus.Aborted:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
break;
default:
assertUnreachable(pullCreditRec.status);
@@ -185,6 +460,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
const newTxState =
computePeerPullCreditTransactionState(pullCreditRec);
await tx.peerPullCredit.put(pullCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -197,10 +473,10 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -221,6 +497,10 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.Aborted:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
break;
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
@@ -233,9 +513,11 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
const oldTxState =
computePeerPullCreditTransactionState(pullCreditRec);
pullCreditRec.status = newStatus;
+ pullCreditRec.failReason = reason;
const newTxState =
computePeerPullCreditTransactionState(pullCreditRec);
await tx.peerPullCredit.put(pullCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -251,7 +533,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -264,12 +546,17 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
case PeerPullPaymentCreditStatus.PendingWithdrawing:
case PeerPullPaymentCreditStatus.PendingReady:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
case PeerPullPaymentCreditStatus.Done:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
case PeerPullPaymentCreditStatus.Aborted:
break;
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ newStatus = PeerPullPaymentCreditStatus.PendingBalanceKycInit;
+ break;
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
break;
@@ -285,6 +572,9 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
break;
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ newStatus = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
+ break;
default:
assertUnreachable(pullCreditRec.status);
}
@@ -295,6 +585,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
const newTxState =
computePeerPullCreditTransactionState(pullCreditRec);
await tx.peerPullCredit.put(pullCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -307,10 +598,10 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
wex.taskScheduler.startShepherdTask(retryTag);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -319,13 +610,19 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
}
let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
switch (pullCreditRec.status) {
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
case PeerPullPaymentCreditStatus.PendingCreatePurse:
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+ pullCreditRec.abortReason = reason;
break;
case PeerPullPaymentCreditStatus.PendingWithdrawing:
throw Error("can't abort anymore");
case PeerPullPaymentCreditStatus.PendingReady:
+ pullCreditRec.abortReason = reason;
newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
break;
case PeerPullPaymentCreditStatus.Done:
@@ -349,6 +646,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
const newTxState =
computePeerPullCreditTransactionState(pullCreditRec);
await tx.peerPullCredit.put(pullCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -373,14 +671,17 @@ async function queryPurseForPeerPullCredit(
);
purseDepositUrl.searchParams.set("timeout_ms", "30000");
logger.info(`querying purse status via ${purseDepositUrl.href}`);
- const resp = await wex.http.fetch(purseDepositUrl.href, {
- timeout: { d_ms: 60000 },
- cancellationToken: wex.cancellationToken,
- });
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullIni.pursePub,
- });
+ const resp = await wex.ws.runLongpollQueueing(
+ wex,
+ purseDepositUrl.hostname,
+ async () => {
+ return await wex.http.fetch(purseDepositUrl.href, {
+ timeout: { d_ms: 60000 },
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
+ const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
logger.info(`purse status code: HTTP ${resp.status}`);
@@ -388,7 +689,7 @@ async function queryPurseForPeerPullCredit(
case HttpStatusCode.Gone: {
// Exchange says that purse doesn't exist anymore => expired!
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!finPi) {
@@ -400,11 +701,12 @@ async function queryPurseForPeerPullCredit(
finPi.status = PeerPullPaymentCreditStatus.Expired;
}
await tx.peerPullCredit.put(finPi);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPullCreditTransactionState(finPi);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
case HttpStatusCode.NotFound:
@@ -452,7 +754,7 @@ async function queryPurseForPeerPullCredit(
},
});
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!finPi) {
@@ -464,11 +766,12 @@ async function queryPurseForPeerPullCredit(
finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing;
}
await tx.peerPullCredit.put(finPi);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPullCreditTransactionState(finPi);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
@@ -476,23 +779,35 @@ async function longpollKycStatus(
wex: WalletExecutionContext,
pursePub: string,
exchangeUrl: string,
- kycInfo: KycPendingInfo,
- userType: KycUserType,
+ kycPaytoHash: string,
): Promise<TaskRunResult> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub,
+ // FIXME: What if this changes? Should be part of the p2p record
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: exchangeUrl,
});
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
- url.searchParams.set("timeout_ms", "10000");
- logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: wex.cancellationToken,
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: mergeReserveInfo.reservePriv,
+ accountPub: mergeReserveInfo.reservePub,
});
+
+ const ctx = new PeerPullCreditTransactionContext(wex, pursePub);
+ const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl);
+ const kycStatusRes = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`kyc url ${url.href}`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
if (
kycStatusRes.status === HttpStatusCode.Ok ||
// FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -500,7 +815,7 @@ async function longpollKycStatus(
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const peerIni = await tx.peerPullCredit.get(pursePub);
if (!peerIni) {
@@ -515,10 +830,11 @@ async function longpollKycStatus(
peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
const newTxState = computePeerPullCreditTransactionState(peerIni);
await tx.peerPullCredit.put(peerIni);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
return TaskRunResult.longpollReturnedPending();
@@ -532,10 +848,7 @@ async function processPeerPullCreditAbortingDeletePurse(
peerPullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
const { pursePub, pursePriv } = peerPullIni;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub,
- });
+ const ctx = new PeerPullCreditTransactionContext(wex, peerPullIni.pursePub);
const sigResp = await wex.cryptoApi.signDeletePurse({
pursePriv,
@@ -558,6 +871,7 @@ async function processPeerPullCreditAbortingDeletePurse(
"denominations",
"coinAvailability",
"coins",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -571,6 +885,7 @@ async function processPeerPullCreditAbortingDeletePurse(
const oldTxState = computePeerPullCreditTransactionState(ppiRec);
ppiRec.status = PeerPullPaymentCreditStatus.Aborted;
await tx.peerPullCredit.put(ppiRec);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPullCreditTransactionState(ppiRec);
return {
oldTxState,
@@ -578,7 +893,7 @@ async function processPeerPullCreditAbortingDeletePurse(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
@@ -591,14 +906,11 @@ async function handlePeerPullCreditWithdrawing(
throw Error("invalid db state (withdrawing, but no withdrawal group ID");
}
await waitWithdrawalFinal(wex, pullIni.withdrawalGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullIni.pursePub,
- });
+ const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
const wgId = pullIni.withdrawalGroupId;
let finished: boolean = false;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit", "withdrawalGroups"] },
+ { storeNames: ["peerPullCredit", "withdrawalGroups", "transactionsMeta"] },
async (tx) => {
const ppi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!ppi) {
@@ -623,6 +935,7 @@ async function handlePeerPullCreditWithdrawing(
// FIXME: Also handle other final states!
}
await tx.peerPullCredit.put(ppi);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPullCreditTransactionState(ppi);
return {
oldTxState,
@@ -630,7 +943,7 @@ async function handlePeerPullCreditWithdrawing(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
if (finished) {
return TaskRunResult.finished();
} else {
@@ -643,6 +956,33 @@ async function handlePeerPullCreditCreatePurse(
wex: WalletExecutionContext,
pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
+ const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
+
+ const kycCheckRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ wex,
+ pullIni.exchangeBaseUrl,
+ pullIni.estimatedAmountEffective,
+ );
+
+ if (kycCheckRes.result === "violation") {
+ // Do this before we transition so that the exchange is already in the right state.
+ await handleStartExchangeWalletKyc(wex, {
+ amount: kycCheckRes.nextThreshold,
+ exchangeBaseUrl: pullIni.exchangeBaseUrl,
+ });
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPullPaymentCreditStatus.PendingCreatePurse) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycInit;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ }
+
const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
const pursePub = pullIni.pursePub;
const mergeReserve = await wex.db.runReadOnlyTx(
@@ -730,22 +1070,17 @@ async function handlePeerPullCreditCreatePurse(
if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
const respJson = await httpResp.json();
- const kycPending = codecForWalletKycUuid().decode(respJson);
+ const kycPending = codecForLegitimizationNeededResponse().decode(respJson);
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
- return processPeerPullCreditKycRequired(wex, pullIni, kycPending);
+ return processPeerPullCreditKycRequired(wex, pullIni, kycPending.h_payto);
}
const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
logger.info(`reserve merge response: ${j2s(resp)}`);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullIni.pursePub,
- });
-
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const pi2 = await tx.peerPullCredit.get(pursePub);
if (!pi2) {
@@ -754,11 +1089,12 @@ async function handlePeerPullCreditCreatePurse(
const oldTxState = computePeerPullCreditTransactionState(pi2);
pi2.status = PeerPullPaymentCreditStatus.PendingReady;
await tx.peerPullCredit.put(pi2);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPullCreditTransactionState(pi2);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
@@ -766,6 +1102,10 @@ export async function processPeerPullCredit(
wex: WalletExecutionContext,
pursePub: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
const pullIni = await wex.db.runReadOnlyTx(
{ storeNames: ["peerPullCredit"] },
async (tx) => {
@@ -783,6 +1123,8 @@ export async function processPeerPullCredit(
logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
+ const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
+
switch (pullIni.status) {
case PeerPullPaymentCreditStatus.Done: {
return TaskRunResult.finished();
@@ -790,15 +1132,14 @@ export async function processPeerPullCredit(
case PeerPullPaymentCreditStatus.PendingReady:
return queryPurseForPeerPullCredit(wex, pullIni);
case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
- if (!pullIni.kycInfo) {
- throw Error("invalid state, kycInfo required");
+ if (!pullIni.kycPaytoHash) {
+ throw Error("invalid state, kycPaytoHash required");
}
return await longpollKycStatus(
wex,
pursePub,
pullIni.exchangeBaseUrl,
- pullIni.kycInfo,
- "individual",
+ pullIni.kycPaytoHash,
);
}
case PeerPullPaymentCreditStatus.PendingCreatePurse:
@@ -807,14 +1148,19 @@ export async function processPeerPullCredit(
return await processPeerPullCreditAbortingDeletePurse(wex, pullIni);
case PeerPullPaymentCreditStatus.PendingWithdrawing:
return handlePeerPullCreditWithdrawing(wex, pullIni);
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ return processPeerPullCreditBalanceKyc(ctx, pullIni);
case PeerPullPaymentCreditStatus.Aborted:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
case PeerPullPaymentCreditStatus.SuspendedReady:
case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
break;
default:
assertUnreachable(pullIni.status);
@@ -823,42 +1169,127 @@ export async function processPeerPullCredit(
return TaskRunResult.finished();
}
+async function processPeerPullCreditBalanceKyc(
+ ctx: PeerPullCreditTransactionContext,
+ peerInc: PeerPullCreditRecord,
+): Promise<TaskRunResult> {
+ const exchangeBaseUrl = peerInc.exchangeBaseUrl;
+ const amount = peerInc.estimatedAmountEffective;
+
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
+ });
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (
+ rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycRequired
+ ) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
+ rec.kycAccessToken = ret.walletKycAccessToken;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
+ }
+}
+
async function processPeerPullCreditKycRequired(
wex: WalletExecutionContext,
peerIni: PeerPullCreditRecord,
- kycPending: WalletKycUuid,
+ kycPayoHash: string,
): Promise<TaskRunResult> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: peerIni.pursePub,
- });
+ const ctx = new PeerPullCreditTransactionContext(wex, peerIni.pursePub);
const { pursePub } = peerIni;
- const userType = "individual";
- const url = new URL(
- `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
- peerIni.exchangeBaseUrl,
- );
+ // FIXME: What if this changes? Should be part of the p2p record
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: peerIni.exchangeBaseUrl,
+ });
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: mergeReserveInfo.reservePriv,
+ accountPub: mergeReserveInfo.reservePub,
+ });
+
+ const url = new URL(`kyc-check/${kycPayoHash}`, peerIni.exchangeBaseUrl);
logger.info(`kyc url ${url.href}`);
const kycStatusRes = await wex.http.fetch(url.href, {
method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
cancellationToken: wex.cancellationToken,
});
if (
kycStatusRes.status === HttpStatusCode.Ok ||
- //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
logger.warn("kyc requested, but already fulfilled");
return TaskRunResult.backoff();
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
+ const kycStatus = await readResponseJsonOrThrow(
+ kycStatusRes,
+ codecForAccountKycStatus(),
+ );
logger.info(`kyc status: ${j2s(kycStatus)}`);
const { transitionInfo, result } = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit"] },
+ { storeNames: ["peerPullCredit", "transactionsMeta"] },
async (tx) => {
const peerInc = await tx.peerPullCredit.get(pursePub);
if (!peerInc) {
@@ -868,33 +1299,23 @@ async function processPeerPullCreditKycRequired(
};
}
const oldTxState = computePeerPullCreditTransactionState(peerInc);
- peerInc.kycInfo = {
- paytoHash: kycPending.h_payto,
- requirementRow: kycPending.requirement_row,
- };
- peerInc.kycUrl = kycStatus.kyc_url;
+ peerInc.kycPaytoHash = kycPayoHash;
+ logger.info(
+ `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`,
+ );
+ peerInc.kycAccessToken = kycStatus.access_token;
peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
const newTxState = computePeerPullCreditTransactionState(peerInc);
await tx.peerPullCredit.put(peerInc);
- // We'll remove this eventually! New clients should rely on the
- // kycUrl field of the transaction, not the error code.
- const res: TaskRunResult = {
- type: TaskRunResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
- {
- kycUrl: kycStatus.kyc_url,
- },
- ),
- };
+ await ctx.updateTransactionMeta(tx);
return {
transitionInfo: { oldTxState, newTxState },
- result: res,
+ result: TaskRunResult.progress(),
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
- return TaskRunResult.backoff();
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ return result;
} else {
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
}
@@ -903,7 +1324,22 @@ async function processPeerPullCreditKycRequired(
/**
* Check fees and available exchanges for a peer push payment initiation.
*/
-export async function checkPeerPullPaymentInitiation(
+export async function checkPeerPullCredit(
+ wex: WalletExecutionContext,
+ req: CheckPeerPullCreditRequest,
+): Promise<CheckPeerPullCreditResponse> {
+ return runWithClientCancellation(
+ wex,
+ "checkPeerPullCredit",
+ req.clientCancellationId,
+ async () => internalCheckPeerPullCredit(wex, req),
+ );
+}
+
+/**
+ * Check fees and available exchanges for a peer push payment initiation.
+ */
+export async function internalCheckPeerPullCredit(
wex: WalletExecutionContext,
req: CheckPeerPullCreditRequest,
): Promise<CheckPeerPullCreditResponse> {
@@ -933,6 +1369,11 @@ export async function checkPeerPullPaymentInitiation(
Amounts.parseOrThrow(req.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to check pull payment from ${exchangeUrl}, can't select denominations for instructed amount (${req.amount}`,
+ );
+ }
logger.trace(`got withdrawal info`);
@@ -1021,7 +1462,8 @@ export async function initiatePeerPullPayment(
const exchangeBaseUrl = maybeExchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
const mergeReserveInfo = await getMergeReserveInfo(wex, {
exchangeBaseUrl: exchangeBaseUrl,
@@ -1052,11 +1494,18 @@ export async function initiatePeerPullPayment(
Amounts.parseOrThrow(req.partialContractTerms.amount),
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to initiate pull payment from ${exchangeBaseUrl}, can't select denominations for instructed amount (${req.partialContractTerms.amount}`,
+ );
+ }
const mergeTimestamp = TalerPreciseTimestamp.now();
+ const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub);
+
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullCredit", "contractTerms"] },
+ { storeNames: ["peerPullCredit", "contractTerms", "transactionsMeta"] },
async (tx) => {
const ppi: PeerPullCreditRecord = {
amount: req.partialContractTerms.amount,
@@ -1076,6 +1525,7 @@ export async function initiatePeerPullPayment(
estimatedAmountEffective: wi.withdrawalAmountEffective,
};
await tx.peerPullCredit.put(ppi);
+ await ctx.updateTransactionMeta(tx);
const oldTxState: TransactionState = {
major: TransactionMajorState.None,
};
@@ -1088,17 +1538,9 @@ export async function initiatePeerPullPayment(
},
);
- const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub);
-
notifyTransition(wex, ctx.transactionId, transitionInfo);
wex.taskScheduler.startShepherdTask(ctx.taskId);
- // The pending-incoming balance has changed.
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
- });
-
return {
talerUri: stringifyTalerUri({
type: TalerUriAction.PayPull,
@@ -1179,6 +1621,26 @@ export function computePeerPullCreditTransactionState(
major: TransactionMajorState.Aborting,
minor: TransactionMinorState.DeletePurse,
};
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
}
}
@@ -1234,5 +1696,13 @@ export function computePeerPullCreditTransactionActions(
return [TransactionAction.Delete];
case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
}
}
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index e9be15026..206962c82 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -32,7 +32,6 @@ import {
ExchangePurseDeposits,
HttpStatusCode,
Logger,
- NotificationType,
ObservabilityEventType,
PeerContractTerms,
PreparePeerPullDebitRequest,
@@ -41,8 +40,10 @@ import {
SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
+ TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolViolationError,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -50,6 +51,7 @@ import {
TransactionState,
TransactionType,
assertUnreachable,
+ checkDbInvariant,
checkLogicInvariant,
codecForAny,
codecForExchangeGetContractResponse,
@@ -59,6 +61,7 @@ import {
encodeCrock,
getRandomBytes,
j2s,
+ makeTalerErrorDetail,
parsePayPullUri,
} from "@gnu-taler/taler-util";
import {
@@ -81,9 +84,13 @@ import {
PeerPullDebitRecordStatus,
PeerPullPaymentIncomingRecord,
RefreshOperationStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
WalletStoresV1,
+ timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
+import { getScopeForAllExchanges } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
@@ -93,6 +100,7 @@ import { DbReadWriteTransaction, StoreNames } from "./query.js";
import { createRefreshGroup } from "./refresh.js";
import {
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
@@ -122,16 +130,79 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
this.peerPullDebitId = peerPullDebitId;
}
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["peerPullDebit", "transactionsMeta"]>,
+ ): Promise<void> {
+ const ppdRec = await tx.peerPullDebit.get(this.peerPullDebitId);
+ if (!ppdRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: ppdRec.status,
+ timestamp: ppdRec.timestampCreated,
+ currency: Amounts.currencyOf(ppdRec.amount),
+ exchanges: [ppdRec.exchangeBaseUrl],
+ });
+ }
+
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
+ if (!pi) {
+ return undefined;
+ }
+ const ort = await tx.operationRetries.get(this.taskId);
+ const txState = computePeerPullDebitTransactionState(pi);
+ const ctRec = await tx.contractTerms.get(pi.contractTermsHash);
+ checkDbInvariant(!!ctRec, `no contract terms for ${this.transactionId}`);
+ const contractTerms = ctRec.contractTermsRaw;
+ return {
+ type: TransactionType.PeerPullDebit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pi.exchangeBaseUrl]),
+ txActions: computePeerPullDebitTransactionActions(pi),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(pi.amount))
+ : pi.coinSel?.totalCost
+ ? pi.coinSel?.totalCost
+ : Amounts.stringify(pi.amount),
+ amountRaw: Amounts.stringify(pi.amount),
+ exchangeBaseUrl: pi.exchangeBaseUrl,
+ info: {
+ expiration: contractTerms.purse_expiration,
+ summary: contractTerms.summary,
+ },
+ abortReason: pi.abortReason,
+ failReason: pi.failReason,
+ timestamp: timestampPreciseFromDb(pi.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId: pi.peerPullDebitId,
+ }),
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+ }
+
async deleteTransaction(): Promise<void> {
const transactionId = this.transactionId;
const ws = this.wex;
const peerPullDebitId = this.peerPullDebitId;
await ws.db.runReadWriteTx(
- { storeNames: ["peerPullDebit", "tombstones"] },
+ { storeNames: ["peerPullDebit", "tombstones", "transactionsMeta"] },
async (tx) => {
const debit = await tx.peerPullDebit.get(peerPullDebitId);
if (debit) {
await tx.peerPullDebit.delete(peerPullDebitId);
+ await this.updateTransactionMeta(tx);
await tx.tombstones.put({ id: transactionId });
}
},
@@ -144,7 +215,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
const wex = this.wex;
const peerPullDebitId = this.peerPullDebitId;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullDebit"] },
+ { storeNames: ["peerPullDebit", "transactionsMeta"] },
async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
@@ -179,6 +250,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
pullDebitRec.status = newStatus;
const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
await tx.peerPullDebit.put(pullDebitRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -213,7 +285,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
this.wex.taskScheduler.startShepherdTask(this.taskId);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const ctx = this;
await ctx.transition(async (pi) => {
switch (pi.status) {
@@ -223,6 +295,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
// FIXME: Should we also abort the corresponding refresh session?!
pi.status = PeerPullDebitRecordStatus.Failed;
+ pi.failReason = reason;
return TransitionResultType.Transition;
default:
return TransitionResultType.Stay;
@@ -231,17 +304,18 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
this.wex.taskScheduler.stopShepherdTask(this.taskId);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const ctx = this;
await ctx.transitionExtra(
{
extraStores: [
"coinAvailability",
+ "coinAvailability",
+ "coinHistory",
+ "coins",
"denominations",
"refreshGroups",
"refreshSessions",
- "coins",
- "coinAvailability",
],
},
async (pi, tx) => {
@@ -277,6 +351,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
pi.abortRefreshGroupId = refresh.refreshGroupId;
+ pi.abortReason = reason;
return TransitionResultType.Transition;
},
);
@@ -301,14 +376,14 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
rec: PeerPullPaymentIncomingRecord,
tx: DbReadWriteTransaction<
typeof WalletStoresV1,
- ["peerPullDebit", ...StoreNameArray]
+ ["peerPullDebit", "transactionsMeta", ...StoreNameArray]
>,
) => Promise<TransitionResultType>,
): Promise<void> {
const wex = this.wex;
const extraStores = opts.extraStores ?? [];
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullDebit", ...extraStores] },
+ { storeNames: ["peerPullDebit", "transactionsMeta", ...extraStores] },
async (tx) => {
const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
if (!pi) {
@@ -319,12 +394,23 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
switch (res) {
case TransitionResultType.Transition: {
await tx.peerPullDebit.put(pi);
+ await this.updateTransactionMeta(tx);
const newTxState = computePeerPullDebitTransactionState(pi);
return {
oldTxState,
newTxState,
};
}
+ case TransitionResultType.Delete: {
+ await tx.peerPullDebit.delete(this.peerPullDebitId);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ }
default:
return undefined;
}
@@ -401,27 +487,31 @@ async function handlePurseCreationConflict(
coinSelRes.result.coins,
);
- await ws.db.runReadWriteTx({ storeNames: ["peerPullDebit"] }, async (tx) => {
- const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
- if (!myPpi) {
- return;
- }
- switch (myPpi.status) {
- case PeerPullDebitRecordStatus.PendingDeposit:
- case PeerPullDebitRecordStatus.SuspendedDeposit: {
- const sel = coinSelRes.result;
- myPpi.coinSel = {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
- totalCost: Amounts.stringify(totalAmount),
- };
- break;
- }
- default:
+ await ws.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "transactionsMeta"] },
+ async (tx) => {
+ const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
+ if (!myPpi) {
return;
- }
- await tx.peerPullDebit.put(myPpi);
- });
+ }
+ switch (myPpi.status) {
+ case PeerPullDebitRecordStatus.PendingDeposit:
+ case PeerPullDebitRecordStatus.SuspendedDeposit: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ break;
+ }
+ default:
+ return;
+ }
+ await tx.peerPullDebit.put(myPpi);
+ await ctx.updateTransactionMeta(tx);
+ },
+ );
return TaskRunResult.backoff();
}
@@ -475,13 +565,15 @@ async function processPeerPullDebitPendingDeposit(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
+ "coinAvailability",
+ "coinHistory",
"coins",
"denominations",
+ "exchanges",
+ "peerPullDebit",
"refreshGroups",
"refreshSessions",
- "peerPullDebit",
- "coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -496,11 +588,7 @@ async function processPeerPullDebitPendingDeposit(
return false;
}
await spendCoins(wex, tx, {
- // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- }),
+ transactionId: ctx.transactionId,
coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
contributions: coinSelRes.result.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
@@ -513,6 +601,7 @@ async function processPeerPullDebitPendingDeposit(
totalCost: Amounts.stringify(totalAmount),
};
await tx.peerPullDebit.put(pi);
+ await ctx.updateTransactionMeta(tx);
return true;
},
);
@@ -573,7 +662,12 @@ async function processPeerPullDebitPendingDeposit(
continue;
}
case HttpStatusCode.Gone: {
- await ctx.abortTransaction();
+ await ctx.abortTransaction(
+ makeTalerErrorDetail(
+ TalerErrorCode.WALLET_PEER_PULL_DEBIT_PURSE_GONE,
+ {},
+ ),
+ );
return TaskRunResult.backoff();
}
case HttpStatusCode.Conflict: {
@@ -608,12 +702,9 @@ async function processPeerPullDebitAbortingRefresh(
const peerPullDebitId = peerPullInc.peerPullDebitId;
const abortRefreshGroupId = peerPullInc.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- });
+ const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPullDebit", "refreshGroups"] },
+ { storeNames: ["peerPullDebit", "refreshGroups", "transactionsMeta"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPullDebitRecordStatus | undefined;
@@ -640,12 +731,13 @@ async function processPeerPullDebitAbortingRefresh(
newDg.status = newOpState;
const newTxState = computePeerPullDebitTransactionState(newDg);
await tx.peerPullDebit.put(newDg);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
}
return undefined;
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
// FIXME: Shouldn't this be finished in some cases?!
return TaskRunResult.backoff();
}
@@ -654,6 +746,10 @@ export async function processPeerPullDebit(
wex: WalletExecutionContext,
peerPullDebitId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
const peerPullInc = await wex.db.runReadOnlyTx(
{ storeNames: ["peerPullDebit"] },
async (tx) => {
@@ -697,6 +793,9 @@ export async function confirmPeerPullDebit(
);
}
+ const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
+ const transactionId = ctx.transactionId;
+
const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
const coinSelRes = await selectPeerCoins(wex, {
@@ -728,18 +827,18 @@ export async function confirmPeerPullDebit(
const totalAmount = await getTotalPeerPaymentCost(wex, coins);
- // FIXME: Missing notification here!
-
- await wex.db.runReadWriteTx(
+ const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
+ "coinAvailability",
+ "coinHistory",
"coins",
"denominations",
+ "exchanges",
+ "peerPullDebit",
"refreshGroups",
"refreshSessions",
- "peerPullDebit",
- "coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -750,13 +849,10 @@ export async function confirmPeerPullDebit(
if (pi.status !== PeerPullDebitRecordStatus.DialogProposed) {
return;
}
+ const oldTxState = computePeerPullDebitTransactionState(pi);
if (coinSelRes.type == "success") {
await spendCoins(wex, tx, {
- // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- }),
+ transactionId,
coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
contributions: coinSelRes.result.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
@@ -770,18 +866,14 @@ export async function confirmPeerPullDebit(
};
}
pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+ await ctx.updateTransactionMeta(tx);
await tx.peerPullDebit.put(pi);
+ const newTxState = computePeerPullDebitTransactionState(pi);
+ return { oldTxState, newTxState };
},
);
- const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
-
- const transactionId = ctx.transactionId;
-
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
+ notifyTransition(wex, transactionId, transitionInfo);
wex.taskScheduler.startShepherdTask(ctx.taskId);
@@ -917,24 +1009,27 @@ export async function preparePeerPullDebit(
const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+ const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
+
await wex.db.runReadWriteTx(
- { storeNames: ["peerPullDebit", "contractTerms"] },
+ { storeNames: ["peerPullDebit", "contractTerms", "transactionsMeta"] },
async (tx) => {
await tx.contractTerms.put({
h: contractTermsHash,
contractTermsRaw: contractTerms,
- }),
- await tx.peerPullDebit.add({
- peerPullDebitId,
- contractPriv: contractPriv,
- exchangeBaseUrl: exchangeBaseUrl,
- pursePub: pursePub,
- timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
- contractTermsHash,
- amount: contractTerms.amount,
- status: PeerPullDebitRecordStatus.DialogProposed,
- totalCostEstimated: Amounts.stringify(totalAmount),
- });
+ });
+ await tx.peerPullDebit.add({
+ peerPullDebitId,
+ contractPriv: contractPriv,
+ exchangeBaseUrl: exchangeBaseUrl,
+ pursePub: pursePub,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ contractTermsHash,
+ amount: contractTerms.amount,
+ status: PeerPullDebitRecordStatus.DialogProposed,
+ totalCostEstimated: Amounts.stringify(totalAmount),
+ });
+ await ctx.updateTransactionMeta(tx);
},
);
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 6d9f329e5..b7eface80 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -15,20 +15,23 @@
*/
import {
+ AbsoluteTime,
AcceptPeerPushPaymentResponse,
Amounts,
ConfirmPeerPushCreditRequest,
ContractTermsUtil,
ExchangePurseMergeRequest,
+ ExchangeWalletKycStatus,
HttpStatusCode,
+ LegitimizationNeededResponse,
Logger,
NotificationType,
PeerContractTerms,
PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse,
- TalerErrorCode,
+ TalerErrorDetail,
TalerPreciseTimestamp,
- TalerProtocolTimestamp,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -36,42 +39,58 @@ import {
TransactionState,
TransactionType,
WalletAccountMergeFlags,
- WalletKycUuid,
assertUnreachable,
checkDbInvariant,
+ codecForAccountKycStatus,
codecForAny,
codecForExchangeGetContractResponse,
+ codecForLegitimizationNeededResponse,
codecForPeerContractTerms,
- codecForWalletKycUuid,
decodeCrock,
eddsaGetPublic,
encodeCrock,
getRandomBytes,
j2s,
- makeErrorDetail,
parsePayPushUri,
talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ readResponseJsonOrThrow,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
TaskIdStr,
+ TaskIdentifiers,
TaskRunResult,
- TaskRunResultType,
TombstoneTag,
TransactionContext,
+ TransitionResult,
+ TransitionResultType,
constructTaskIdentifier,
+ genericWaitForStateVal,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
- KycPendingInfo,
- KycUserType,
+ OperationRetryRecord,
PeerPushCreditStatus,
PeerPushPaymentIncomingRecord,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ WalletDbStoresArr,
+ WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
-import { fetchFreshExchange } from "./exchanges.js";
+import {
+ BalanceThresholdCheckResult,
+ checkIncomingAmountLegalUnderKycBalanceThreshold,
+ fetchFreshExchange,
+ getScopeForAllExchanges,
+ handleStartExchangeWalletKyc,
+} from "./exchanges.js";
import {
codecForExchangePurseStatus,
getMergeReserveInfo,
@@ -79,6 +98,7 @@ import {
import {
TransitionInfo,
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
@@ -111,10 +131,225 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
});
}
+ /**
+ * Transition a peer-push-credit transaction.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transition<StoreNameArray extends WalletDbStoresArr = []>(
+ opts: { extraStores?: StoreNameArray; transactionLabel?: string },
+ f: (
+ rec: PeerPushPaymentIncomingRecord | undefined,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "peerPushCredit",
+ "transactionsMeta",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ...StoreNameArray,
+ ]
+ >,
+ ) => Promise<TransitionResult<PeerPushPaymentIncomingRecord>>,
+ ): Promise<TransitionInfo | undefined> {
+ const baseStores = [
+ "peerPushCredit" as const,
+ "transactionsMeta" as const,
+ "operationRetries" as const,
+ "exchanges" as const,
+ "exchangeDetails" as const,
+ ];
+ const stores = opts.extraStores
+ ? [...baseStores, ...opts.extraStores]
+ : baseStores;
+
+ let errorThrown: Error | undefined;
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: stores },
+ async (tx) => {
+ const rec = await tx.peerPushCredit.get(this.peerPushCreditId);
+ let oldTxState: TransactionState;
+ if (rec) {
+ oldTxState = computePeerPushCreditTransactionState(rec);
+ } else {
+ oldTxState = {
+ major: TransactionMajorState.None,
+ };
+ }
+ let res: TransitionResult<PeerPushPaymentIncomingRecord> | undefined;
+ try {
+ res = await f(rec, tx);
+ } catch (error) {
+ if (error instanceof Error) {
+ errorThrown = error;
+ }
+ return undefined;
+ }
+
+ switch (res.type) {
+ case TransitionResultType.Transition: {
+ await tx.peerPushCredit.put(res.rec);
+ await this.updateTransactionMeta(tx);
+ const newTxState = computePeerPushCreditTransactionState(res.rec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ case TransitionResultType.Delete:
+ await tx.peerPushCredit.delete(this.peerPushCreditId);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ default:
+ return undefined;
+ }
+ },
+ );
+ if (errorThrown) {
+ throw errorThrown;
+ }
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ return transitionInfo;
+ }
+
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["peerPushCredit", "transactionsMeta"]>,
+ ): Promise<void> {
+ const ppdRec = await tx.peerPushCredit.get(this.peerPushCreditId);
+ if (!ppdRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: ppdRec.status,
+ timestamp: ppdRec.timestamp,
+ currency: Amounts.currencyOf(ppdRec.estimatedAmountEffective),
+ exchanges: [ppdRec.exchangeBaseUrl],
+ });
+ }
+
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const pushInc = await tx.peerPushCredit.get(this.peerPushCreditId);
+ if (!pushInc) {
+ return undefined;
+ }
+
+ let wg: WithdrawalGroupRecord | undefined = undefined;
+ let wgRetryRecord: OperationRetryRecord | undefined = undefined;
+ if (pushInc.withdrawalGroupId) {
+ wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
+ if (wg) {
+ const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
+ wgRetryRecord = await tx.operationRetries.get(withdrawalOpId);
+ }
+ }
+ const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
+ const pushRetryRecord = await tx.operationRetries.get(pushIncOpId);
+
+ const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
+
+ if (!ct) {
+ throw Error("contract terms for P2P payment not found");
+ }
+
+ const peerContractTerms = ct.contractTermsRaw;
+
+ let kycUrl: string | undefined = undefined;
+ if (wg?.kycAccessToken && wg.exchangeBaseUrl) {
+ kycUrl = new URL(`kyc-spa/${wg.kycAccessToken}`, wg.exchangeBaseUrl).href;
+ }
+
+ if (wg) {
+ if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
+ throw Error("invalid withdrawal group type for push payment credit");
+ }
+ checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
+
+ const txState = computePeerPushCreditTransactionState(pushInc);
+ return {
+ type: TransactionType.PeerPushCredit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pushInc.exchangeBaseUrl]),
+ txActions: computePeerPushCreditTransactionActions(pushInc),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount))
+ : Amounts.stringify(wg.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wg.instructedAmount),
+ exchangeBaseUrl: wg.exchangeBaseUrl,
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ timestamp: timestampPreciseFromDb(wg.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: pushInc.peerPushCreditId,
+ }),
+ abortReason: pushInc.abortReason,
+ failReason: pushInc.failReason,
+ kycUrl,
+ kycPaytoHash: wg.kycPaytoHash,
+ ...(wgRetryRecord?.lastError ? { error: wgRetryRecord.lastError } : {}),
+ };
+ }
+
+ const txState = computePeerPushCreditTransactionState(pushInc);
+ return {
+ type: TransactionType.PeerPushCredit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pushInc.exchangeBaseUrl]),
+ txActions: computePeerPushCreditTransactionActions(pushInc),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
+ : // FIXME: This is wrong, needs to consider fees!
+ Amounts.stringify(peerContractTerms.amount),
+ amountRaw: Amounts.stringify(peerContractTerms.amount),
+ exchangeBaseUrl: pushInc.exchangeBaseUrl,
+ info: {
+ expiration: peerContractTerms.purse_expiration,
+ summary: peerContractTerms.summary,
+ },
+ kycUrl,
+ kycPaytoHash: pushInc.kycPaytoHash,
+ timestamp: timestampPreciseFromDb(pushInc.timestamp),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushCredit,
+ peerPushCreditId: pushInc.peerPushCreditId,
+ }),
+ abortReason: pushInc.abortReason,
+ failReason: pushInc.failReason,
+ ...(pushRetryRecord?.lastError
+ ? { error: pushRetryRecord.lastError }
+ : {}),
+ };
+ }
+
async deleteTransaction(): Promise<void> {
const { wex, peerPushCreditId } = this;
await wex.db.runReadWriteTx(
- { storeNames: ["withdrawalGroups", "peerPushCredit", "tombstones"] },
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "peerPushCredit",
+ "tombstones",
+ "transactionsMeta",
+ ],
+ },
async (tx) => {
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) {
@@ -132,6 +367,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
}
}
await tx.peerPushCredit.delete(peerPushCreditId);
+ await this.updateTransactionMeta(tx);
await tx.tombstones.put({
id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
});
@@ -141,63 +377,54 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
}
async suspendTransaction(): Promise<void> {
- const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
- async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushCreditRec) {
- logger.warn(`peer push credit ${peerPushCreditId} not found`);
- return;
- }
- let newStatus: PeerPushCreditStatus | undefined = undefined;
- switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
- case PeerPushCreditStatus.SuspendedMerge:
- case PeerPushCreditStatus.SuspendedMergeKycRequired:
- case PeerPushCreditStatus.SuspendedWithdrawing:
- break;
- case PeerPushCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
- break;
- case PeerPushCreditStatus.PendingMerge:
- newStatus = PeerPushCreditStatus.SuspendedMerge;
- break;
- case PeerPushCreditStatus.PendingWithdrawing:
- // FIXME: Suspend internal withdrawal transaction!
- newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
- break;
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.Failed:
- break;
- default:
- assertUnreachable(pushCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- },
- );
- notifyTransition(wex, transactionId, transitionInfo);
- wex.taskScheduler.stopShepherdTask(retryTag);
+ await this.transition({}, async (pushCreditRec) => {
+ if (!pushCreditRec) {
+ return TransitionResult.stay();
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ case PeerPushCreditStatus.Failed:
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ newStatus = PeerPushCreditStatus.SuspendedBalanceKycInit;
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.SuspendedMerge;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ // FIXME: Suspend internal withdrawal transaction!
+ newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ pushCreditRec.status = newStatus;
+ return TransitionResult.transition(pushCreditRec);
+ } else {
+ return TransitionResult.stay();
+ }
+ });
}
async abortTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
+ { storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -206,29 +433,23 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
}
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
+ case PeerPushCreditStatus.Failed:
+ case PeerPushCreditStatus.Aborted:
case PeerPushCreditStatus.Done:
break;
case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.DialogProposed:
case PeerPushCreditStatus.SuspendedMergeKycRequired:
case PeerPushCreditStatus.SuspendedWithdrawing:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.PendingWithdrawing:
case PeerPushCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
newStatus = PeerPushCreditStatus.Aborted;
break;
- case PeerPushCreditStatus.PendingWithdrawing:
- newStatus = PeerPushCreditStatus.Aborted;
- break;
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.Failed:
- break;
default:
assertUnreachable(pushCreditRec.status);
}
@@ -239,6 +460,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
const newTxState =
computePeerPushCreditTransactionState(pushCreditRec);
await tx.peerPushCredit.put(pushCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -254,7 +476,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
+ { storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -264,10 +486,15 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.PendingMergeKycRequired:
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.Aborted:
+ case PeerPushCreditStatus.Failed:
+ break;
case PeerPushCreditStatus.SuspendedMerge:
newStatus = PeerPushCreditStatus.PendingMerge;
break;
@@ -278,9 +505,11 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
// FIXME: resume underlying "internal-withdrawal" transaction.
newStatus = PeerPushCreditStatus.PendingWithdrawing;
break;
- case PeerPushCreditStatus.Aborted:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ newStatus = PeerPushCreditStatus.PendingBalanceKycRequired;
break;
- case PeerPushCreditStatus.Failed:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ newStatus = PeerPushCreditStatus.PendingBalanceKycInit;
break;
default:
assertUnreachable(pushCreditRec.status);
@@ -292,6 +521,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
const newTxState =
computePeerPushCreditTransactionState(pushCreditRec);
await tx.peerPushCredit.put(pushCreditRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -304,17 +534,17 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
wex.taskScheduler.startShepherdTask(retryTag);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
+ { storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
}
- let newStatus: PeerPushCreditStatus | undefined = undefined;
+ let newStatus: PeerPushCreditStatus;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.Aborted:
@@ -328,24 +558,25 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
case PeerPushCreditStatus.SuspendedMerge:
case PeerPushCreditStatus.SuspendedMergeKycRequired:
case PeerPushCreditStatus.SuspendedWithdrawing:
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
newStatus = PeerPushCreditStatus.Failed;
break;
default:
assertUnreachable(pushCreditRec.status);
}
- if (newStatus != null) {
- const oldTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
+ const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ pushCreditRec.status = newStatus;
+ pushCreditRec.failReason = reason;
+ const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
+ await tx.peerPushCredit.put(pushCreditRec);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState,
+ };
},
);
wex.taskScheduler.stopShepherdTask(retryTag);
@@ -364,6 +595,10 @@ export async function preparePeerPushCredit(
throw Error("got invalid taler://pay-push URI");
}
+ // add exchange entry if it doesn't exist already!
+ const exchangeBaseUrl = uri.exchangeBaseUrl;
+ await fetchFreshExchange(wex, exchangeBaseUrl);
+
const existing = await wex.db.runReadOnlyTx(
{ storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
@@ -405,10 +640,6 @@ export async function preparePeerPushCredit(
};
}
- const exchangeBaseUrl = uri.exchangeBaseUrl;
-
- await fetchFreshExchange(wex, exchangeBaseUrl);
-
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
@@ -429,12 +660,12 @@ export async function preparePeerPushCredit(
pursePub: pursePub,
});
+ const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
+
const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
const purseHttpResp = await wex.http.fetch(getPurseUrl.href);
- const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
-
const purseStatus = await readSuccessResponseJsonOrThrow(
purseHttpResp,
codecForExchangePurseStatus(),
@@ -459,8 +690,16 @@ export async function preparePeerPushCredit(
undefined,
);
+ if (wi.selectedDenoms.selectedDenoms.length === 0) {
+ throw Error(
+ `unable to prepare push credit from ${exchangeBaseUrl}, can't select denominations for instructed amount (${purseStatus.balance}`,
+ );
+ }
+
+ const ctx = new PeerPushCreditTransactionContext(wex, withdrawalGroupId);
+
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["contractTerms", "peerPushCredit"] },
+ { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
async (tx) => {
const rec: PeerPushPaymentIncomingRecord = {
peerPushCreditId,
@@ -478,6 +717,7 @@ export async function preparePeerPushCredit(
),
};
await tx.peerPushCredit.add(rec);
+ await ctx.updateTransactionMeta(tx);
await tx.contractTerms.put({
h: contractTermsHash,
contractTermsRaw: dec.contractTerms,
@@ -501,11 +741,6 @@ export async function preparePeerPushCredit(
notifyTransition(wex, transactionId, transitionInfo);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
-
return {
amount: purseStatus.balance,
amountEffective: wi.withdrawalAmountEffective,
@@ -521,31 +756,43 @@ async function longpollKycStatus(
wex: WalletExecutionContext,
peerPushCreditId: string,
exchangeUrl: string,
- kycInfo: KycPendingInfo,
- userType: KycUserType,
+ kycPaytoHash: string,
): Promise<TaskRunResult> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
+ const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
+
+ // FIXME: What if this changes? Should be part of the p2p record
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: exchangeUrl,
});
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
- url.searchParams.set("timeout_ms", "30000");
- logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: wex.cancellationToken,
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: mergeReserveInfo.reservePriv,
+ accountPub: mergeReserveInfo.reservePub,
});
+
+ const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl);
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
+
if (
kycStatusRes.status === HttpStatusCode.Ok ||
- //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
+ { storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -558,13 +805,14 @@ async function longpollKycStatus(
peerInc.status = PeerPushCreditStatus.PendingMerge;
const newTxState = computePeerPushCreditTransactionState(peerInc);
await tx.peerPushCredit.put(peerInc);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.progress();
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- // FIXME: Do we have to update the URL here?
+ // Access token / URL stays the same, just long-poll again.
return TaskRunResult.longpollReturnedPending();
} else {
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
@@ -574,39 +822,58 @@ async function longpollKycStatus(
async function processPeerPushCreditKycRequired(
wex: WalletExecutionContext,
peerInc: PeerPushPaymentIncomingRecord,
- kycPending: WalletKycUuid,
+ kycPending: LegitimizationNeededResponse,
): Promise<TaskRunResult> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: peerInc.peerPushCreditId,
});
+ const ctx = new PeerPushCreditTransactionContext(
+ wex,
+ peerInc.peerPushCreditId,
+ );
const { peerPushCreditId } = peerInc;
- const userType = "individual";
+ // FIXME: What if this changes? Should be part of the p2p record
+ const mergeReserveInfo = await getMergeReserveInfo(wex, {
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ });
+
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: mergeReserveInfo.reservePriv,
+ accountPub: mergeReserveInfo.reservePub,
+ });
+
const url = new URL(
- `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
+ `kyc-check/${kycPending.h_payto}`,
peerInc.exchangeBaseUrl,
);
logger.info(`kyc url ${url.href}`);
const kycStatusRes = await wex.http.fetch(url.href, {
method: "GET",
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
cancellationToken: wex.cancellationToken,
});
+ logger.info(`kyc result status ${kycStatusRes.status}`);
+
if (
kycStatusRes.status === HttpStatusCode.Ok ||
- //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
logger.warn("kyc requested, but already fulfilled");
return TaskRunResult.finished();
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const statusResp = await readResponseJsonOrThrow(
+ kycStatusRes,
+ codecForAccountKycStatus(),
+ );
+ logger.info(`kyc status: ${j2s(statusResp)}`);
const { transitionInfo, result } = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit"] },
+ { storeNames: ["peerPushCredit", "transactionsMeta"] },
async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -616,28 +883,15 @@ async function processPeerPushCreditKycRequired(
};
}
const oldTxState = computePeerPushCreditTransactionState(peerInc);
- peerInc.kycInfo = {
- paytoHash: kycPending.h_payto,
- requirementRow: kycPending.requirement_row,
- };
- peerInc.kycUrl = kycStatus.kyc_url;
+ peerInc.kycPaytoHash = kycPending.h_payto;
+ peerInc.kycAccessToken = statusResp.access_token;
peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired;
const newTxState = computePeerPushCreditTransactionState(peerInc);
await tx.peerPushCredit.put(peerInc);
- // We'll remove this eventually! New clients should rely on the
- // kycUrl field of the transaction, not the error code.
- const res: TaskRunResult = {
- type: TaskRunResultType.Error,
- errorDetail: makeErrorDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
- {
- kycUrl: kycStatus.kyc_url,
- },
- ),
- };
+ await ctx.updateTransactionMeta(tx);
return {
transitionInfo: { oldTxState, newTxState },
- result: res,
+ result: TaskRunResult.progress(),
};
},
);
@@ -654,18 +908,45 @@ async function handlePendingMerge(
contractTerms: PeerContractTerms,
): Promise<TaskRunResult> {
const { peerPushCreditId } = peerInc;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
+ const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
+
+ const kycCheckRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ wex,
+ peerInc.exchangeBaseUrl,
+ peerInc.estimatedAmountEffective,
+ );
+
+ if (kycCheckRes.result === "violation") {
+ // Do this before we transition so that the exchange is already in the right state.
+ await handleStartExchangeWalletKyc(wex, {
+ amount: kycCheckRes.nextThreshold,
+ exchangeBaseUrl: peerInc.exchangeBaseUrl,
+ });
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPushCreditStatus.PendingMerge) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPushCreditStatus.PendingBalanceKycInit;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ }
const amount = Amounts.parseOrThrow(contractTerms.amount);
+ // FIXME: What if this changes? Should be part of the p2p record
const mergeReserveInfo = await getMergeReserveInfo(wex, {
exchangeBaseUrl: peerInc.exchangeBaseUrl,
});
- const mergeTimestamp = TalerProtocolTimestamp.now();
+ const timestamp = timestampPreciseFromDb(peerInc.timestamp);
+
+ const mergeTimestamp = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.fromPreciseTimestamp(timestamp),
+ );
const reservePayto = talerPaytoFromExchangeReserve(
peerInc.exchangeBaseUrl,
@@ -703,10 +984,14 @@ async function handlePendingMerge(
});
if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
- const respJson = await mergeHttpResp.json();
- const kycPending = codecForWalletKycUuid().decode(respJson);
- logger.info(`kyc uuid response: ${j2s(kycPending)}`);
- return processPeerPushCreditKycRequired(wex, peerInc, kycPending);
+ const kycLegiNeededResp = await readResponseJsonOrThrow(
+ mergeHttpResp,
+ codecForLegitimizationNeededResponse(),
+ );
+ logger.info(
+ `kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`,
+ );
+ return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp);
}
logger.trace(`merge request: ${j2s(mergeReq)}`);
@@ -739,6 +1024,7 @@ async function handlePendingMerge(
"reserves",
"exchanges",
"exchangeDetails",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -764,6 +1050,7 @@ async function handlePendingMerge(
}
}
await tx.peerPushCredit.put(peerInc);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPushCreditTransactionState(peerInc);
return {
peerPushCreditTransition: { oldTxState, newTxState },
@@ -780,7 +1067,7 @@ async function handlePendingMerge(
withdrawalGroupPrep.transactionId,
txRes?.wgCreateRes?.transitionInfo,
);
- notifyTransition(wex, transactionId, txRes?.peerPushCreditTransition);
+ notifyTransition(wex, ctx.transactionId, txRes?.peerPushCreditTransition);
return TaskRunResult.backoff();
}
@@ -793,14 +1080,14 @@ async function handlePendingWithdrawing(
throw Error("invalid db state (withdrawing, but no withdrawal group ID");
}
await waitWithdrawalFinal(wex, peerInc.withdrawalGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: peerInc.peerPushCreditId,
- });
+ const ctx = new PeerPushCreditTransactionContext(
+ wex,
+ peerInc.peerPushCreditId,
+ );
const wgId = peerInc.withdrawalGroupId;
let finished: boolean = false;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit", "withdrawalGroups"] },
+ { storeNames: ["peerPushCredit", "withdrawalGroups", "transactionsMeta"] },
async (tx) => {
const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
if (!ppi) {
@@ -825,6 +1112,7 @@ async function handlePendingWithdrawing(
// FIXME: Also handle other final states!
}
await tx.peerPushCredit.put(ppi);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPushCreditTransactionState(ppi);
return {
oldTxState,
@@ -832,7 +1120,7 @@ async function handlePendingWithdrawing(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
if (finished) {
return TaskRunResult.finished();
} else {
@@ -845,10 +1133,15 @@ export async function processPeerPushCredit(
wex: WalletExecutionContext,
peerPushCreditId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+ const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
+
let peerInc: PeerPushPaymentIncomingRecord | undefined;
let contractTerms: PeerContractTerms | undefined;
await wex.db.runReadWriteTx(
- { storeNames: ["contractTerms", "peerPushCredit"] },
+ { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
async (tx) => {
peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -859,6 +1152,7 @@ export async function processPeerPushCredit(
contractTerms = ctRec.contractTermsRaw;
}
await tx.peerPushCredit.put(peerInc);
+ await ctx.updateTransactionMeta(tx);
},
);
@@ -879,35 +1173,111 @@ export async function processPeerPushCredit(
switch (peerInc.status) {
case PeerPushCreditStatus.PendingMergeKycRequired: {
- if (!peerInc.kycInfo) {
- throw Error("invalid state, kycInfo required");
+ if (!peerInc.kycPaytoHash) {
+ throw Error("invalid state, kycPaytoHash required");
}
return await longpollKycStatus(
wex,
peerPushCreditId,
peerInc.exchangeBaseUrl,
- peerInc.kycInfo,
- "individual",
+ peerInc.kycPaytoHash,
);
}
-
- case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingMerge: {
return handlePendingMerge(wex, peerInc, contractTerms);
-
- case PeerPushCreditStatus.PendingWithdrawing:
+ }
+ case PeerPushCreditStatus.PendingWithdrawing: {
return handlePendingWithdrawing(wex, peerInc);
-
+ }
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.PendingBalanceKycRequired: {
+ return await processPeerPushCreditBalanceKyc(ctx, peerInc);
+ }
default:
return TaskRunResult.finished();
}
}
+async function processPeerPushCreditBalanceKyc(
+ ctx: PeerPushCreditTransactionContext,
+ peerInc: PeerPushPaymentIncomingRecord,
+): Promise<TaskRunResult> {
+ const exchangeBaseUrl = peerInc.exchangeBaseUrl;
+ const amount = peerInc.estimatedAmountEffective;
+
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
+ });
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPushCreditStatus.PendingMerge;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPushCreditStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPushCreditStatus.PendingBalanceKycRequired;
+ rec.kycAccessToken = ret.walletKycAccessToken;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
+ }
+}
+
export async function confirmPeerPushCredit(
wex: WalletExecutionContext,
req: ConfirmPeerPushCreditRequest,
): Promise<AcceptPeerPushPaymentResponse> {
- let peerInc: PeerPushPaymentIncomingRecord | undefined;
- let peerPushCreditId: string;
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx) {
throw Error("invalid transaction ID");
@@ -915,21 +1285,26 @@ export async function confirmPeerPushCredit(
if (parsedTx.tag !== TransactionType.PeerPushCredit) {
throw Error("invalid transaction ID type");
}
- peerPushCreditId = parsedTx.peerPushCreditId;
+ const ctx = new PeerPushCreditTransactionContext(
+ wex,
+ parsedTx.peerPushCreditId,
+ );
- logger.trace(`confirming peer-push-credit ${peerPushCreditId}`);
+ logger.trace(`confirming peer-push-credit ${ctx.peerPushCreditId}`);
- await wex.db.runReadWriteTx(
- { storeNames: ["contractTerms", "peerPushCredit"] },
+ const peerInc = await wex.db.runReadWriteTx(
+ { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
async (tx) => {
- peerInc = await tx.peerPushCredit.get(peerPushCreditId);
- if (!peerInc) {
+ const rec = await tx.peerPushCredit.get(ctx.peerPushCreditId);
+ if (!rec) {
return;
}
- if (peerInc.status === PeerPushCreditStatus.DialogProposed) {
- peerInc.status = PeerPushCreditStatus.PendingMerge;
+ if (rec.status === PeerPushCreditStatus.DialogProposed) {
+ rec.status = PeerPushCreditStatus.PendingMerge;
}
- await tx.peerPushCredit.put(peerInc);
+ await tx.peerPushCredit.put(rec);
+ await ctx.updateTransactionMeta(tx);
+ return rec;
},
);
@@ -939,17 +1314,13 @@ export async function confirmPeerPushCredit(
);
}
- const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
+ const exchange = await fetchFreshExchange(wex, peerInc.exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
wex.taskScheduler.startShepherdTask(ctx.taskId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId,
- });
-
return {
- transactionId,
+ transactionId: ctx.transactionId,
};
}
@@ -974,7 +1345,7 @@ export function computePeerPushCreditTransactionState(
case PeerPushCreditStatus.PendingMergeKycRequired:
return {
major: TransactionMajorState.Pending,
- minor: TransactionMinorState.KycRequired,
+ minor: TransactionMinorState.MergeKycRequired,
};
case PeerPushCreditStatus.PendingWithdrawing:
return {
@@ -1004,6 +1375,26 @@ export function computePeerPushCreditTransactionState(
return {
major: TransactionMajorState.Failed,
};
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
default:
assertUnreachable(pushCreditRecord.status);
}
@@ -1041,6 +1432,14 @@ export function computePeerPushCreditTransactionActions(
return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedWithdrawing:
return [TransactionAction.Resume, TransactionAction.Fail];
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushCreditStatus.Failed:
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index f8e6adb3c..8ff4920ed 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -30,9 +30,11 @@ import {
SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
+ TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TalerProtocolViolationError,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -45,13 +47,18 @@ import {
encodeCrock,
getRandomBytes,
j2s,
+ stringifyPayPushUri,
} from "@gnu-taler/taler-util";
import {
HttpResponse,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
+import {
+ PreviousPayCoins,
+ selectPeerCoins,
+ selectPeerCoinsInTx,
+} from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -59,6 +66,7 @@ import {
TaskRunResultType,
TransactionContext,
constructTaskIdentifier,
+ runWithClientCancellation,
spendCoins,
} from "./common.js";
import { EncryptContractRequest } from "./crypto/cryptoTypes.js";
@@ -66,18 +74,24 @@ import {
PeerPushDebitRecord,
PeerPushDebitStatus,
RefreshOperationStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbReadWriteTransaction,
+ timestampPreciseFromDb,
timestampPreciseToDb,
timestampProtocolFromDb,
timestampProtocolToDb,
} from "./db.js";
+import { getScopeForAllExchanges } from "./exchanges.js";
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
+ getTotalPeerPaymentCostInTx,
queryCoinInfosForSelection,
} from "./pay-peer-common.js";
import { createRefreshGroup, waitRefreshFinal } from "./refresh.js";
import {
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
} from "./transactions.js";
import { WalletExecutionContext } from "./wallet.js";
@@ -102,14 +116,91 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
});
}
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>,
+ ): Promise<void> {
+ const ppdRec = await tx.peerPushDebit.get(this.pursePub);
+ if (!ppdRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: ppdRec.status,
+ timestamp: ppdRec.timestampCreated,
+ currency: Amounts.currencyOf(ppdRec.amount),
+ exchanges: [ppdRec.exchangeBaseUrl],
+ });
+ }
+
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const pushDebitRec = await tx.peerPushDebit.get(this.pursePub);
+ if (!pushDebitRec) {
+ return undefined;
+ }
+ const retryRec = await tx.operationRetries.get(this.taskId);
+
+ const ctRec = await tx.contractTerms.get(pushDebitRec.contractTermsHash);
+ checkDbInvariant(
+ !!ctRec,
+ `no contract terms for p2p push ${this.pursePub}`,
+ );
+
+ const contractTerms = ctRec.contractTermsRaw;
+
+ let talerUri: string | undefined = undefined;
+ switch (pushDebitRec.status) {
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ talerUri = stringifyPayPushUri({
+ exchangeBaseUrl: pushDebitRec.exchangeBaseUrl,
+ contractPriv: pushDebitRec.contractPriv,
+ });
+ }
+ const txState = computePeerPushDebitTransactionState(pushDebitRec);
+ return {
+ type: TransactionType.PeerPushDebit,
+ txState,
+ scopes: await getScopeForAllExchanges(tx, [pushDebitRec.exchangeBaseUrl]),
+ txActions: computePeerPushDebitTransactionActions(pushDebitRec),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(pushDebitRec.totalCost))
+ : pushDebitRec.totalCost,
+ amountRaw: pushDebitRec.amount,
+ exchangeBaseUrl: pushDebitRec.exchangeBaseUrl,
+ info: {
+ expiration: contractTerms.purse_expiration,
+ summary: contractTerms.summary,
+ },
+ timestamp: timestampPreciseFromDb(pushDebitRec.timestampCreated),
+ talerUri,
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: pushDebitRec.pursePub,
+ }),
+ failReason: pushDebitRec.failReason,
+ abortReason: pushDebitRec.abortReason,
+ ...(retryRec?.lastError ? { error: retryRec.lastError } : {}),
+ };
+ }
+
async deleteTransaction(): Promise<void> {
const { wex, pursePub, transactionId } = this;
await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit", "tombstones"] },
+ { storeNames: ["peerPushDebit", "tombstones", "transactionsMeta"] },
async (tx) => {
const debit = await tx.peerPushDebit.get(pursePub);
if (debit) {
await tx.peerPushDebit.delete(pursePub);
+ await this.updateTransactionMeta(tx);
await tx.tombstones.put({ id: transactionId });
}
},
@@ -119,7 +210,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit"] },
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -162,6 +253,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushDebit.put(pushDebitRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -174,10 +266,10 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit"] },
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -188,11 +280,13 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
switch (pushDebitRec.status) {
case PeerPushDebitStatus.PendingReady:
case PeerPushDebitStatus.SuspendedReady:
+ pushDebitRec.abortReason = reason;
newStatus = PeerPushDebitStatus.AbortingDeletePurse;
break;
case PeerPushDebitStatus.SuspendedCreatePurse:
case PeerPushDebitStatus.PendingCreatePurse:
// Network request might already be in-flight!
+ pushDebitRec.abortReason = reason;
newStatus = PeerPushDebitStatus.AbortingDeletePurse;
break;
case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
@@ -215,6 +309,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushDebit.put(pushDebitRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -231,7 +326,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit"] },
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -274,6 +369,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
pushDebitRec.status = newStatus;
const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
await tx.peerPushDebit.put(pushDebitRec);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState,
@@ -286,17 +382,17 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
notifyTransition(wex, transactionId, transitionInfo);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit"] },
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
logger.warn(`peer push debit ${pursePub} not found`);
return;
}
- let newStatus: PeerPushDebitStatus | undefined = undefined;
+ let newStatus: PeerPushDebitStatus;
switch (pushDebitRec.status) {
case PeerPushDebitStatus.AbortingRefreshDeleted:
case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
@@ -318,21 +414,20 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
case PeerPushDebitStatus.Failed:
case PeerPushDebitStatus.Expired:
// Do nothing
- break;
+ return undefined;
default:
assertUnreachable(pushDebitRec.status);
}
- if (newStatus != null) {
- const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
- pushDebitRec.status = newStatus;
- const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
- await tx.peerPushDebit.put(pushDebitRec);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
+ const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ pushDebitRec.status = newStatus;
+ pushDebitRec.failReason = reason;
+ const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
+ await tx.peerPushDebit.put(pushDebitRec);
+ await this.updateTransactionMeta(tx);
+ return {
+ oldTxState,
+ newTxState,
+ };
},
);
wex.taskScheduler.stopShepherdTask(retryTag);
@@ -345,12 +440,26 @@ export async function checkPeerPushDebit(
wex: WalletExecutionContext,
req: CheckPeerPushDebitRequest,
): Promise<CheckPeerPushDebitResponse> {
+ return runWithClientCancellation(
+ wex,
+ "checkPeerPushDebit",
+ req.clientCancellationId,
+ () => internalCheckPeerPushDebit(wex, req),
+ );
+}
+
+async function internalCheckPeerPushDebit(
+ wex: WalletExecutionContext,
+ req: CheckPeerPushDebitRequest,
+): Promise<CheckPeerPushDebitResponse> {
const instructedAmount = Amounts.parseOrThrow(req.amount);
logger.trace(
`checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
);
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount,
+ restrictScope: req.restrictScope,
+ feesCoveredByCounterparty: false,
});
let coins: SelectedProspectiveCoin[] | undefined = undefined;
switch (coinSelRes.type) {
@@ -423,8 +532,10 @@ async function handlePurseCreationConflict(
}
const coinSelRes = await selectPeerCoins(wex, {
- instructedAmount,
+ instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+ restrictScope: peerPushInitiation.restrictScope,
repair,
+ feesCoveredByCounterparty: false,
});
switch (coinSelRes.type) {
@@ -440,26 +551,30 @@ async function handlePurseCreationConflict(
assertUnreachable(coinSelRes);
}
- await wex.db.runReadWriteTx({ storeNames: ["peerPushDebit"] }, async (tx) => {
- const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub);
- if (!myPpi) {
- return;
- }
- switch (myPpi.status) {
- case PeerPushDebitStatus.PendingCreatePurse:
- case PeerPushDebitStatus.SuspendedCreatePurse: {
- const sel = coinSelRes.result;
- myPpi.coinSel = {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
- };
- break;
- }
- default:
+ await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
+ async (tx) => {
+ const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub);
+ if (!myPpi) {
return;
- }
- await tx.peerPushDebit.put(myPpi);
- });
+ }
+ switch (myPpi.status) {
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.SuspendedCreatePurse: {
+ const sel = coinSelRes.result;
+ myPpi.coinSel = {
+ coinPubs: sel.coins.map((x) => x.coinPub),
+ contributions: sel.coins.map((x) => x.contribution),
+ };
+ break;
+ }
+ default:
+ return;
+ }
+ await tx.peerPushDebit.put(myPpi);
+ await ctx.updateTransactionMeta(tx);
+ },
+ );
return TaskRunResult.progress();
}
@@ -491,6 +606,8 @@ async function processPeerPushDebitCreateReserve(
if (!peerPushInitiation.coinSel) {
const coinSelRes = await selectPeerCoins(wex, {
instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+ restrictScope: peerPushInitiation.restrictScope,
+ feesCoveredByCounterparty: false,
});
switch (coinSelRes.type) {
@@ -511,14 +628,16 @@ async function processPeerPushDebitCreateReserve(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
- "contractTerms",
- "coins",
"coinAvailability",
+ "coinHistory",
+ "coins",
+ "contractTerms",
"denominations",
+ "exchanges",
+ "peerPushDebit",
"refreshGroups",
"refreshSessions",
- "peerPushDebit",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -538,10 +657,7 @@ async function processPeerPushDebitCreateReserve(
// we might want to mark the coins as used and spend them
// after we've been able to create the purse.
await spendCoins(wex, tx, {
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub,
- }),
+ transactionId: ctx.transactionId,
coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
contributions: coinSelRes.result.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
@@ -550,6 +666,7 @@ async function processPeerPushDebitCreateReserve(
});
await tx.peerPushDebit.put(ppi);
+ await ctx.updateTransactionMeta(tx);
return true;
},
);
@@ -559,11 +676,13 @@ async function processPeerPushDebitCreateReserve(
return TaskRunResult.backoff();
}
+ const purseAmount = peerPushInitiation.amount;
+
const purseSigResp = await wex.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: peerPushInitiation.mergePub,
minAge: 0,
- purseAmount: peerPushInitiation.amount,
+ purseAmount,
purseExpiration: timestampProtocolFromDb(purseExpiration),
pursePriv: peerPushInitiation.pursePriv,
});
@@ -610,7 +729,8 @@ async function processPeerPushDebitCreateReserve(
);
const reqBody = {
- amount: peerPushInitiation.amount,
+ // Older wallets do not have amountPurse
+ amount: purseAmount,
merge_pub: peerPushInitiation.mergePub,
purse_sig: purseSigResp.sig,
h_contract_terms: hContractTerms,
@@ -635,6 +755,8 @@ async function processPeerPushDebitCreateReserve(
// Possibly on to the next batch.
continue;
case HttpStatusCode.Forbidden: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ logger.error(`${j2s(errResp)}`);
// FIXME: Store this error!
await ctx.failTransaction();
return TaskRunResult.finished();
@@ -670,7 +792,7 @@ async function processPeerPushDebitCreateReserve(
switch (httpResp.status) {
case HttpStatusCode.Ok:
// Possibly on to the next batch.
- continue;
+ break;
case HttpStatusCode.Forbidden: {
// FIXME: Store this error!
await ctx.failTransaction();
@@ -693,6 +815,22 @@ async function processPeerPushDebitCreateReserve(
// All batches done!
+ const getPurseUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+
+ const purseHttpResp = await wex.http.fetch(getPurseUrl.href);
+
+ const purseStatus = await readSuccessResponseJsonOrThrow(
+ purseHttpResp,
+ codecForExchangePurseStatus(),
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`purse status: ${j2s(purseStatus)}`);
+ }
+
await transitionPeerPushDebitTransaction(wex, pursePub, {
stFrom: PeerPushDebitStatus.PendingCreatePurse,
stTo: PeerPushDebitStatus.PendingReady,
@@ -706,10 +844,7 @@ async function processPeerPushDebitAbortingDeletePurse(
peerPushInitiation: PeerPushDebitRecord,
): Promise<TaskRunResult> {
const { pursePub, pursePriv } = peerPushInitiation;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
const sigResp = await wex.cryptoApi.signDeletePurse({
pursePriv,
@@ -730,12 +865,14 @@ async function processPeerPushDebitAbortingDeletePurse(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"peerPushDebit",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
- "coins",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -767,11 +904,12 @@ async function processPeerPushDebitAbortingDeletePurse(
currency,
coinPubs,
RefreshReason.AbortPeerPushDebit,
- transactionId,
+ ctx.transactionId,
);
ppiRec.status = PeerPushDebitStatus.AbortingRefreshDeleted;
ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
await tx.peerPushDebit.put(ppiRec);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
oldTxState,
@@ -779,7 +917,7 @@ async function processPeerPushDebitAbortingDeletePurse(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
}
@@ -795,12 +933,9 @@ async function transitionPeerPushDebitTransaction(
pursePub: string,
transitionSpec: SimpleTransition,
): Promise<void> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit"] },
+ { storeNames: ["peerPushDebit", "transactionsMeta"] },
async (tx) => {
const ppiRec = await tx.peerPushDebit.get(pursePub);
if (!ppiRec) {
@@ -812,6 +947,7 @@ async function transitionPeerPushDebitTransaction(
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
ppiRec.status = transitionSpec.stTo;
await tx.peerPushDebit.put(ppiRec);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
oldTxState,
@@ -819,7 +955,7 @@ async function transitionPeerPushDebitTransaction(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
}
async function processPeerPushDebitAbortingRefreshDeleted(
@@ -829,15 +965,12 @@ async function processPeerPushDebitAbortingRefreshDeleted(
const pursePub = peerPushInitiation.pursePub;
const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: peerPushInitiation.pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
if (peerPushInitiation.abortRefreshGroupId) {
await waitRefreshFinal(wex, peerPushInitiation.abortRefreshGroupId);
}
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["refreshGroups", "peerPushDebit"] },
+ { storeNames: ["refreshGroups", "peerPushDebit", "transactionsMeta"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPushDebitStatus | undefined;
@@ -864,12 +997,13 @@ async function processPeerPushDebitAbortingRefreshDeleted(
newDg.status = newOpState;
const newTxState = computePeerPushDebitTransactionState(newDg);
await tx.peerPushDebit.put(newDg);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
}
return undefined;
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
// FIXME: Shouldn't this be finished in some cases?!
return TaskRunResult.backoff();
}
@@ -881,12 +1015,9 @@ async function processPeerPushDebitAbortingRefreshExpired(
const pursePub = peerPushInitiation.pursePub;
const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
checkLogicInvariant(!!abortRefreshGroupId);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: peerPushInitiation.pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit", "refreshGroups"] },
+ { storeNames: ["peerPushDebit", "refreshGroups", "transactionsMeta"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPushDebitStatus | undefined;
@@ -913,12 +1044,13 @@ async function processPeerPushDebitAbortingRefreshExpired(
newDg.status = newOpState;
const newTxState = computePeerPushDebitTransactionState(newDg);
await tx.peerPushDebit.put(newDg);
+ await ctx.updateTransactionMeta(tx);
return { oldTxState, newTxState };
}
return undefined;
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
// FIXME: Shouldn't this be finished in some cases?!
return TaskRunResult.backoff();
}
@@ -932,20 +1064,23 @@ async function processPeerPushDebitReady(
): Promise<TaskRunResult> {
logger.trace("processing peer-push-debit pending(ready)");
const pursePub = peerPushInitiation.pursePub;
- const transactionId = constructTaskIdentifier({
- tag: PendingTaskType.PeerPushDebit,
- pursePub,
- });
+ const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
const mergeUrl = new URL(
`purses/${pursePub}/merge`,
peerPushInitiation.exchangeBaseUrl,
);
- mergeUrl.searchParams.set("timeout_ms", "30000");
- logger.info(`long-polling on purse status at ${mergeUrl.href}`);
- const resp = await wex.http.fetch(mergeUrl.href, {
- // timeout: getReserveRequestTimeout(withdrawalGroup),
- cancellationToken: wex.cancellationToken,
- });
+ const resp = await wex.ws.runLongpollQueueing(
+ wex,
+ mergeUrl.hostname,
+ async (timeoutMs) => {
+ mergeUrl.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`long-polling on purse status at ${mergeUrl.href}`);
+ return await wex.http.fetch(mergeUrl.href, {
+ // timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
if (resp.status === HttpStatusCode.Ok) {
const purseStatus = await readSuccessResponseJsonOrThrow(
resp,
@@ -971,12 +1106,14 @@ async function processPeerPushDebitReady(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"peerPushDebit",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
- "coins",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1005,13 +1142,14 @@ async function processPeerPushDebitReady(
currency,
coinPubs,
RefreshReason.AbortPeerPushDebit,
- transactionId,
+ ctx.transactionId,
);
ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
}
ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired;
await tx.peerPushDebit.put(ppiRec);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
oldTxState,
@@ -1019,7 +1157,7 @@ async function processPeerPushDebitReady(
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
return TaskRunResult.backoff();
} else {
logger.warn(`unexpected HTTP status for purse: ${resp.status}`);
@@ -1031,6 +1169,10 @@ export async function processPeerPushDebit(
wex: WalletExecutionContext,
pursePub: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
const peerPushInitiation = await wex.db.runReadOnlyTx(
{ storeNames: ["peerPushDebit"] },
async (tx) => {
@@ -1080,48 +1222,13 @@ export async function initiatePeerPushDebit(
req.partialContractTerms.amount,
);
const purseExpiration = req.partialContractTerms.purse_expiration;
- const contractTerms = req.partialContractTerms;
+ const contractTerms = { ...req.partialContractTerms };
const pursePair = await wex.cryptoApi.createEddsaKeypair({});
const mergePair = await wex.cryptoApi.createEddsaKeypair({});
- const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
-
const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
- const coinSelRes = await selectPeerCoins(wex, {
- instructedAmount,
- });
-
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
-
- switch (coinSelRes.type) {
- case "failure":
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
- case "success":
- coins = coinSelRes.result.coins;
- break;
- default:
- assertUnreachable(coinSelRes);
- }
-
- const sel = coinSelRes.result;
-
- logger.info(`selected p2p coins (push):`);
- logger.trace(`${j2s(coinSelRes)}`);
-
- const totalAmount = await getTotalPeerPaymentCost(wex, coins);
-
- logger.info(`computed total peer payment cost`);
-
const pursePub = pursePair.pub;
const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
@@ -1130,22 +1237,71 @@ export async function initiatePeerPushDebit(
const contractEncNonce = encodeCrock(getRandomBytes(24));
- const transitionInfo = await wex.db.runReadWriteTx(
+ const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
+
+ const res = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
- "contractTerms",
- "coins",
"coinAvailability",
+ "coinHistory",
+ "coins",
+ "contractTerms",
"denominations",
+ "exchangeDetails",
+ "exchanges",
+ "peerPushDebit",
"refreshGroups",
"refreshSessions",
- "peerPushDebit",
+ "transactionsMeta",
],
},
async (tx) => {
+ const coinSelRes = await selectPeerCoinsInTx(wex, tx, {
+ instructedAmount,
+ restrictScope: req.restrictScope,
+ feesCoveredByCounterparty: false,
+ });
+
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ logger.trace(j2s(coinSelRes));
+
+ const sel = coinSelRes.result;
+
+ logger.trace(
+ `peer debit instructed amount: ${Amounts.stringify(instructedAmount)}`,
+ );
+ logger.trace(
+ `peer debit contract terms amount: ${Amounts.stringify(
+ contractTerms.amount,
+ )}`,
+ );
+ logger.trace(
+ `peer debit deposit fees: ${Amounts.stringify(sel.totalDepositFees)}`,
+ );
+
+ const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins);
const ppi: PeerPushDebitRecord = {
amount: Amounts.stringify(instructedAmount),
+ restrictScope: req.restrictScope,
contractPriv: contractKeyPair.priv,
contractPub: contractKeyPair.pub,
contractTermsHash: hContractTerms,
@@ -1170,10 +1326,7 @@ export async function initiatePeerPushDebit(
// we might want to mark the coins as used and spend them
// after we've been able to create the purse.
await spendCoins(wex, tx, {
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: pursePair.pub,
- }),
+ transactionId: ctx.transactionId,
coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
contributions: coinSelRes.result.coins.map((x) =>
Amounts.parseOrThrow(x.contribution),
@@ -1183,6 +1336,7 @@ export async function initiatePeerPushDebit(
}
await tx.peerPushDebit.add(ppi);
+ await ctx.updateTransactionMeta(tx);
await tx.contractTerms.put({
h: hContractTerms,
@@ -1191,16 +1345,15 @@ export async function initiatePeerPushDebit(
const newTxState = computePeerPushDebitTransactionState(ppi);
return {
- oldTxState: { major: TransactionMajorState.None },
- newTxState,
+ transitionInfo: {
+ oldTxState: { major: TransactionMajorState.None },
+ newTxState,
+ },
+ exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
};
},
);
- notifyTransition(wex, transactionId, transitionInfo);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
+ notifyTransition(wex, transactionId, res.transitionInfo);
wex.taskScheduler.startShepherdTask(ctx.taskId);
@@ -1208,7 +1361,7 @@ export async function initiatePeerPushDebit(
contractPriv: contractKeyPair.priv,
mergePriv: mergePair.priv,
pursePub: pursePair.pub,
- exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
+ exchangeBaseUrl: res.exchangeBaseUrl,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: pursePair.pub,
diff --git a/packages/taler-wallet-core/src/query.ts b/packages/taler-wallet-core/src/query.ts
index dc15bbdd1..28011b0ff 100644
--- a/packages/taler-wallet-core/src/query.ts
+++ b/packages/taler-wallet-core/src/query.ts
@@ -41,6 +41,7 @@ import {
Codec,
Logger,
openPromise,
+ safeStringifyException,
} from "@gnu-taler/taler-util";
const logger = new Logger("query.ts");
@@ -76,16 +77,42 @@ export interface IndexOptions {
unique?: boolean;
}
+/**
+ * Log extra stuff that would be too verbose even
+ * on loglevel TRACE.
+ */
+const logExtra = false;
+
+let idbRequestPromId = 1;
+
function requestToPromise(req: IDBRequest): Promise<any> {
+ const myId = idbRequestPromId++;
+ if (logExtra) {
+ logger.trace(`started db request ${myId}`);
+ }
const stack = Error("Failed request was started here.");
return new Promise((resolve, reject) => {
req.onsuccess = () => {
+ if (logExtra) {
+ logger.trace(`finished db request ${myId} with success`);
+ }
resolve(req.result);
};
req.onerror = () => {
- console.error("error in DB request", req.error);
+ if (logExtra) {
+ logger.trace(`finished db request ${myId} with error`);
+ }
+ if (
+ req.error != null &&
+ "name" in req.error &&
+ req.error.name === "AbortError"
+ ) {
+ logger.warn("DB request failed, transaction aborted");
+ } else {
+ logger.error(`error in DB request: ${req.error}`);
+ logger.error(`Request failed: ${stack}`);
+ }
reject(req.error);
- console.error("Request failed:", stack);
};
});
}
@@ -565,34 +592,66 @@ function runTx<Arg, Res>(
arg: Arg,
f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
triggerContext: InternalTriggerContext,
+ cancellationToken: CancellationToken,
): Promise<Res> {
+ // Create stack trace in case we need to to print later where
+ // the transaction was started.
const stack = Error("Failed transaction was started here.");
+
+ const unregisterOnCancelled = cancellationToken.onCancelled(() => {
+ tx.abort();
+ });
+
return new Promise((resolve, reject) => {
let funResult: any = undefined;
let gotFunResult = false;
let transactionException: any = undefined;
+ let aborted = false;
tx.oncomplete = () => {
+ logger.trace("transaction completed");
// This is a fatal error: The transaction completed *before*
// the transaction function returned. Likely, the transaction
// function waited on a promise that is *not* resolved in the
// microtask queue, thus triggering the auto-commit behavior.
// Unfortunately, the auto-commit behavior of IDB can't be switched
- // of. There are some proposals to add this functionality in the future.
+ // off. There are some proposals to add this functionality in the future.
if (!gotFunResult) {
const msg =
"BUG: transaction closed before transaction function returned";
logger.error(msg);
logger.error(`${stack.stack}`);
reject(Error(msg));
+ } else {
+ resolve(funResult);
}
triggerContext.handleAfterCommit();
- resolve(funResult);
+ unregisterOnCancelled();
};
tx.onerror = () => {
+ logger.trace("transaction had error");
+ if (cancellationToken.isCancelled) {
+ reject(
+ new CancellationToken.CancellationError(cancellationToken.reason),
+ );
+ return;
+ }
logger.error("error in transaction");
logger.error(`${stack.stack}`);
+ const txError = tx.error;
+ if (txError) {
+ reject(txError);
+ } else {
+ reject(new Error("unknown transaction error"));
+ }
};
tx.onabort = () => {
+ logger.trace("transaction was aborted");
+ if (cancellationToken.isCancelled) {
+ reject(
+ new CancellationToken.CancellationError(cancellationToken.reason),
+ );
+ return;
+ }
let msg: string;
if (tx.error) {
msg = `Transaction aborted (transaction error): ${tx.error}`;
@@ -601,6 +660,8 @@ function runTx<Arg, Res>(
} else {
msg = "Transaction aborted (no DB error)";
}
+ aborted = true;
+ unregisterOnCancelled();
logger.error(msg);
logger.error(`${stack.stack}`);
reject(new TransactionAbortedError(msg));
@@ -608,21 +669,25 @@ function runTx<Arg, Res>(
const resP = Promise.resolve().then(() => f(arg, tx));
resP
.then((result) => {
+ logger.trace("transaction function returned");
gotFunResult = true;
funResult = result;
})
.catch((e) => {
if (e == TransactionAbort) {
logger.trace("aborting transaction");
+ tx.abort();
+ } else if ("name" in e && e.name === "AbortError") {
+ logger.warn("got AbortError, transaction was aborted");
} else {
transactionException = e;
- console.error("Transaction failed:", e);
- console.error(stack);
+ logger.error(`Transaction failed: ${safeStringifyException(e)}`);
+ logger.error(`${stack}`);
tx.abort();
}
})
.catch((e) => {
- console.error("fatal: aborting transaction failed", e);
+ logger.error(`aborting failed: ${safeStringifyException(e)}`);
});
});
}
@@ -797,9 +862,26 @@ function makeWriteContext(
return ctx;
}
+/**
+ * Handle for typed access to a database.
+ */
export interface DbAccess<StoreMap> {
+ /**
+ * The underlying IndexedDB database handle.
+ *
+ * Use with caution, as using the handle directly will not
+ * properly run DB triggers.
+ */
idbHandle(): IDBDatabase;
+ /**
+ * Run an async function in a "readwrite" transaction on the database, using
+ * all object store.
+ *
+ * The transaction function must run within the microtask queue.
+ * Waiting for macrotasks results in an autocommit and
+ * a subsequent exception thrown by this function.
+ */
runAllStoresReadWriteTx<T>(
options: {
label?: string;
@@ -809,6 +891,14 @@ export interface DbAccess<StoreMap> {
) => Promise<T>,
): Promise<T>;
+ /**
+ * Run an async function in a "readonly" transaction on the database, using
+ * all object store.
+ *
+ * The transaction function must run within the microtask queue.
+ * Waiting for macrotasks results in an autocommit and
+ * a subsequent exception thrown by this function.
+ */
runAllStoresReadOnlyTx<T>(
options: {
label?: string;
@@ -818,6 +908,14 @@ export interface DbAccess<StoreMap> {
) => Promise<T>,
): Promise<T>;
+ /**
+ * Run an async function in a "readwrite" transaction on the database, using
+ * the selected object store.
+ *
+ * The transaction function must run within the microtask queue.
+ * Waiting for macrotasks results in an autocommit and
+ * a subsequent exception thrown by this function.
+ */
runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
opts: {
storeNames: StoreNameArray;
@@ -826,6 +924,14 @@ export interface DbAccess<StoreMap> {
txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T>;
+ /**
+ * Run an async function in a "readonly" transaction on the database, using
+ * the selected object store.
+ *
+ * The transaction function must run within the microtask queue.
+ * Waiting for macrotasks results in an autocommit and
+ * a subsequent exception thrown by this function.
+ */
runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
opts: {
storeNames: StoreNameArray;
@@ -895,7 +1001,7 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
return this.db;
}
- runAllStoresReadWriteTx<T>(
+ async runAllStoresReadWriteTx<T>(
options: {
label?: string;
},
@@ -903,6 +1009,7 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
) => Promise<T>,
): Promise<T> {
+ this.cancellationToken.throwIfCancelled();
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
@@ -919,7 +1026,13 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
);
const tx = this.db.transaction(strStoreNames, mode);
const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
- return runTx(tx, writeContext, txf, triggerContext);
+ return await runTx(
+ tx,
+ writeContext,
+ txf,
+ triggerContext,
+ this.cancellationToken,
+ );
}
async runAllStoresReadOnlyTx<T>(
@@ -930,6 +1043,7 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
) => Promise<T>,
): Promise<T> {
+ this.cancellationToken.throwIfCancelled();
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
@@ -946,7 +1060,13 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
);
const tx = this.db.transaction(strStoreNames, mode);
const writeContext = makeReadContext(tx, accessibleStores, triggerContext);
- const res = await runTx(tx, writeContext, txf, triggerContext);
+ const res = await runTx(
+ tx,
+ writeContext,
+ txf,
+ triggerContext,
+ this.cancellationToken,
+ );
return res;
}
@@ -956,6 +1076,7 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
},
txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
+ this.cancellationToken.throwIfCancelled();
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
@@ -972,16 +1093,23 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
);
const tx = this.db.transaction(strStoreNames, mode);
const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
- const res = await runTx(tx, writeContext, txf, triggerContext);
+ const res = await runTx(
+ tx,
+ writeContext,
+ txf,
+ triggerContext,
+ this.cancellationToken,
+ );
return res;
}
- runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ async runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
opts: {
storeNames: StoreNameArray;
},
txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
+ this.cancellationToken.throwIfCancelled();
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
@@ -998,7 +1126,13 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
);
const tx = this.db.transaction(strStoreNames, mode);
const readContext = makeReadContext(tx, accessibleStores, triggerContext);
- const res = runTx(tx, readContext, txf, triggerContext);
+ const res = await runTx(
+ tx,
+ readContext,
+ txf,
+ triggerContext,
+ this.cancellationToken,
+ );
return res;
}
}
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index be5731b0b..e74e49118 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -30,6 +30,7 @@ import {
Logger,
RefreshReason,
TalerPreciseTimestamp,
+ Transaction,
TransactionIdStr,
TransactionType,
URL,
@@ -54,6 +55,7 @@ import {
RecoupGroupRecord,
RecoupOperationStatus,
RefreshCoinSource,
+ WalletDbAllStoresReadOnlyTransaction,
WalletDbReadWriteTransaction,
WithdrawCoinSource,
WithdrawalGroupStatus,
@@ -65,7 +67,7 @@ import { constructTransactionIdentifier } from "./transactions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
-export const logger = new Logger("operations/recoup.ts");
+const logger = new Logger("operations/recoup.ts");
/**
* Store a recoup group record in the database after marking
@@ -74,11 +76,19 @@ export const logger = new Logger("operations/recoup.ts");
export async function putGroupAsFinished(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["recoupGroups", "denominations", "refreshGroups", "coins"]
+ [
+ "recoupGroups",
+ "denominations",
+ "refreshGroups",
+ "coins",
+ "exchanges",
+ "transactionsMeta",
+ ]
>,
recoupGroup: RecoupGroupRecord,
coinIdx: number,
): Promise<void> {
+ const ctx = new RecoupTransactionContext(wex, recoupGroup.recoupGroupId);
logger.trace(
`setting coin ${coinIdx} of ${recoupGroup.coinPubs.length} as finished`,
);
@@ -87,6 +97,7 @@ export async function putGroupAsFinished(
}
recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
await tx.recoupGroups.put(recoupGroup);
+ await ctx.updateTransactionMeta(tx);
}
async function recoupRewardCoin(
@@ -99,7 +110,16 @@ async function recoupRewardCoin(
// Thus we just put the coin to sleep.
// FIXME: somehow report this to the user
await wex.db.runReadWriteTx(
- { storeNames: ["recoupGroups", "denominations", "refreshGroups", "coins"] },
+ {
+ storeNames: [
+ "recoupGroups",
+ "denominations",
+ "refreshGroups",
+ "coins",
+ "exchanges",
+ "transactionsMeta",
+ ],
+ },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -168,7 +188,16 @@ async function recoupRefreshCoin(
}
await wex.db.runReadWriteTx(
- { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "recoupGroups",
+ "refreshGroups",
+ "transactionsMeta",
+ "exchanges",
+ ],
+ },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -199,24 +228,20 @@ async function recoupRefreshCoin(
revokedCoin.exchangeBaseUrl,
revokedCoin.denomPubHash,
);
- checkDbInvariant(!!oldCoinDenom, `no denom for coin, hash ${oldCoin.denomPubHash}`);
- checkDbInvariant(!!revokedCoinDenom, `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`);
+ checkDbInvariant(
+ !!oldCoinDenom,
+ `no denom for coin, hash ${oldCoin.denomPubHash}`,
+ );
+ checkDbInvariant(
+ !!revokedCoinDenom,
+ `no revoked denom for coin, hash ${revokedCoin.denomPubHash}`,
+ );
revokedCoin.status = CoinStatus.Dormant;
- if (!revokedCoin.spendAllocation) {
- // We don't know what happened to this coin
- logger.error(
- `can't refresh-recoup coin ${revokedCoin.coinPub}, no spendAllocation known`,
- );
- } else {
- let residualAmount = Amounts.sub(
- revokedCoinDenom.value,
- revokedCoin.spendAllocation.amount,
- ).amount;
- recoupGroup.scheduleRefreshCoins.push({
- coinPub: oldCoin.coinPub,
- amount: Amounts.stringify(residualAmount),
- });
- }
+ // FIXME: Schedule recoup for the sum of refreshes, based on the coin event history.
+ // recoupGroup.scheduleRefreshCoins.push({
+ // coinPub: oldCoin.coinPub,
+ // amount: Amounts.stringify(refreshAmount),
+ // });
await tx.coins.put(revokedCoin);
await tx.coins.put(oldCoin);
await putGroupAsFinished(wex, tx, recoupGroup, coinIdx);
@@ -276,7 +301,16 @@ export async function recoupWithdrawCoin(
// FIXME: verify that our expectations about the amount match
await wex.db.runReadWriteTx(
- { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "recoupGroups",
+ "refreshGroups",
+ "transactionsMeta",
+ "exchanges",
+ ],
+ },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -300,6 +334,10 @@ export async function processRecoupGroup(
wex: WalletExecutionContext,
recoupGroupId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
let recoupGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["recoupGroups"] },
async (tx) => {
@@ -393,15 +431,20 @@ export async function processRecoupGroup(
});
}
+ const ctx = new RecoupTransactionContext(wex, recoupGroupId);
+
await wex.db.runReadWriteTx(
{
storeNames: [
- "recoupGroups",
"coinAvailability",
+ "coinHistory",
+ "coins",
"denominations",
+ "recoupGroups",
"refreshGroups",
"refreshSessions",
- "coins",
+ "transactionsMeta",
+ "exchanges",
],
},
async (tx) => {
@@ -425,55 +468,99 @@ export async function processRecoupGroup(
);
}
await tx.recoupGroups.put(rg2);
+ await ctx.updateTransactionMeta(tx);
},
);
return TaskRunResult.finished();
}
export class RecoupTransactionContext implements TransactionContext {
+ public transactionId: TransactionIdStr;
+ public taskId: TaskIdStr;
+
+ constructor(
+ public wex: WalletExecutionContext,
+ private recoupGroupId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Recoup,
+ recoupGroupId,
+ });
+ this.taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Recoup,
+ recoupGroupId,
+ });
+ }
+
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<
+ ["recoupGroups", "exchanges", "transactionsMeta"]
+ >,
+ ): Promise<void> {
+ const recoupRec = await tx.recoupGroups.get(this.recoupGroupId);
+ if (!recoupRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ const exch = await tx.exchanges.get(recoupRec.exchangeBaseUrl);
+ if (!exch || !exch.detailsPointer) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: recoupRec.operationStatus,
+ timestamp: recoupRec.timestampStarted,
+ currency: exch.detailsPointer?.currency,
+ exchanges: [recoupRec.exchangeBaseUrl],
+ });
+ }
+
abortTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
suspendTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
resumeTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
failTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
+
deleteTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
- public transactionId: TransactionIdStr;
- public taskId: TaskIdStr;
- constructor(
- public wex: WalletExecutionContext,
- private recoupGroupId: string,
- ) {
- this.transactionId = constructTransactionIdentifier({
- tag: TransactionType.Recoup,
- recoupGroupId,
- });
- this.taskId = constructTaskIdentifier({
- tag: PendingTaskType.Recoup,
- recoupGroupId,
- });
+ lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ throw new Error("Method not implemented.");
}
}
export async function createRecoupGroup(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["recoupGroups", "denominations", "refreshGroups", "coins"]
+ [
+ "recoupGroups",
+ "denominations",
+ "refreshGroups",
+ "coins",
+ "exchanges",
+ "transactionsMeta",
+ ]
>,
exchangeBaseUrl: string,
coinPubs: string[],
): Promise<string> {
const recoupGroupId = encodeCrock(getRandomBytes(32));
+ const ctx = new RecoupTransactionContext(wex, recoupGroupId);
const recoupGroup: RecoupGroupRecord = {
recoupGroupId,
exchangeBaseUrl: exchangeBaseUrl,
@@ -496,8 +583,7 @@ export async function createRecoupGroup(
}
await tx.recoupGroups.put(recoupGroup);
-
- const ctx = new RecoupTransactionContext(wex, recoupGroupId);
+ await ctx.updateTransactionMeta(tx);
wex.taskScheduler.startShepherdTask(ctx.taskId);
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 05c65f6b6..033a85adf 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -54,10 +54,10 @@ import {
makeErrorDetail,
NotificationType,
RefreshReason,
- TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
+ Transaction,
TransactionAction,
TransactionIdStr,
TransactionMajorState,
@@ -93,6 +93,7 @@ import {
import { CryptoApiStoppedError } from "./crypto/workers/crypto-dispatcher.js";
import {
CoinAvailabilityRecord,
+ CoinHistoryRecord,
CoinRecord,
CoinSourceType,
DenominationRecord,
@@ -101,14 +102,18 @@ import {
RefreshGroupRecord,
RefreshOperationStatus,
RefreshSessionRecord,
+ timestampPreciseFromDb,
timestampPreciseToDb,
+ WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletDbStoresArr,
} from "./db.js";
import { selectWithdrawalDenominations } from "./denomSelection.js";
+import { getScopeForAllExchanges } from "./exchanges.js";
import {
constructTransactionIdentifier,
+ isUnsuccessfulTransaction,
notifyTransition,
TransitionInfo,
} from "./transactions.js";
@@ -121,23 +126,6 @@ import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
const logger = new Logger("refresh.ts");
-/**
- * Update the materialized refresh transaction based
- * on the refresh group record.
- */
-async function updateRefreshTransaction(
- ctx: RefreshTransactionContext,
- tx: WalletDbReadWriteTransaction<
- [
- "refreshGroups",
- "transactions",
- "operationRetries",
- "exchanges",
- "exchangeDetails",
- ]
- >,
-): Promise<void> {}
-
export class RefreshTransactionContext implements TransactionContext {
readonly transactionId: TransactionIdStr;
readonly taskId: TaskIdStr;
@@ -156,6 +144,76 @@ export class RefreshTransactionContext implements TransactionContext {
});
}
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["refreshGroups", "transactionsMeta"]>,
+ ): Promise<void> {
+ const rgRec = await tx.refreshGroups.get(this.refreshGroupId);
+ if (!rgRec) {
+ await tx.transactionsMeta.delete(this.transactionId);
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: this.transactionId,
+ status: rgRec.operationStatus,
+ timestamp: rgRec.timestampCreated,
+ currency: rgRec.currency,
+ exchanges: Object.keys(rgRec.infoPerExchange ?? {}),
+ });
+ }
+
+ /**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const refreshGroupRecord = await tx.refreshGroups.get(this.refreshGroupId);
+ if (!refreshGroupRecord) {
+ return undefined;
+ }
+ const ort = await tx.operationRetries.get(this.taskId);
+ const inputAmount = Amounts.sumOrZero(
+ refreshGroupRecord.currency,
+ refreshGroupRecord.inputPerCoin,
+ ).amount;
+ const outputAmount = Amounts.sumOrZero(
+ refreshGroupRecord.currency,
+ refreshGroupRecord.expectedOutputPerCoin,
+ ).amount;
+ const txState = computeRefreshTransactionState(refreshGroupRecord);
+ return {
+ type: TransactionType.Refresh,
+ txState,
+ scopes: await getScopeForAllExchanges(
+ tx,
+ !refreshGroupRecord.infoPerExchange
+ ? []
+ : Object.keys(refreshGroupRecord.infoPerExchange),
+ ),
+ txActions: computeRefreshTransactionActions(refreshGroupRecord),
+ refreshReason: refreshGroupRecord.reason,
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(inputAmount))
+ : Amounts.stringify(Amounts.sub(outputAmount, inputAmount).amount),
+ amountRaw: Amounts.stringify(
+ Amounts.zeroOfCurrency(refreshGroupRecord.currency),
+ ),
+ refreshInputAmount: Amounts.stringify(inputAmount),
+ refreshOutputAmount: Amounts.stringify(outputAmount),
+ originatingTransactionId: refreshGroupRecord.originatingTransactionId,
+ timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId: refreshGroupRecord.refreshGroupId,
+ }),
+ failReason: refreshGroupRecord.failReason,
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+ }
+
/**
* Transition a withdrawal transaction.
* Extra object stores may be accessed during the transition.
@@ -167,7 +225,7 @@ export class RefreshTransactionContext implements TransactionContext {
tx: WalletDbReadWriteTransaction<
[
"refreshGroups",
- "transactions",
+ "transactionsMeta",
"operationRetries",
"exchanges",
"exchangeDetails",
@@ -178,7 +236,7 @@ export class RefreshTransactionContext implements TransactionContext {
): Promise<TransitionInfo | undefined> {
const baseStores = [
"refreshGroups" as const,
- "transactions" as const,
+ "transactionsMeta" as const,
"operationRetries" as const,
"exchanges" as const,
"exchangeDetails" as const,
@@ -202,7 +260,7 @@ export class RefreshTransactionContext implements TransactionContext {
switch (res.type) {
case TransitionResultType.Transition: {
await tx.refreshGroups.put(res.rec);
- await updateRefreshTransaction(this, tx);
+ await this.updateTransactionMeta(tx);
const newTxState = computeRefreshTransactionState(res.rec);
return {
oldTxState,
@@ -211,7 +269,7 @@ export class RefreshTransactionContext implements TransactionContext {
}
case TransitionResultType.Delete:
await tx.refreshGroups.delete(this.refreshGroupId);
- await updateRefreshTransaction(this, tx);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState: {
@@ -289,7 +347,7 @@ export class RefreshTransactionContext implements TransactionContext {
});
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
await this.transition({}, async (rec, tx) => {
if (!rec) {
return TransitionResult.stay();
@@ -301,6 +359,7 @@ export class RefreshTransactionContext implements TransactionContext {
case RefreshOperationStatus.Pending:
case RefreshOperationStatus.Suspended: {
rec.operationStatus = RefreshOperationStatus.Failed;
+ rec.failReason = reason;
return TransitionResult.transition(rec);
}
default:
@@ -354,6 +413,11 @@ export function getTotalRefreshCostInternal(
refreshedDenom: DenominationInfo,
amountLeft: AmountJson,
): AmountJson {
+ logger.trace(
+ `computing total refresh cost, denom value ${
+ refreshedDenom.value
+ }, amount left ${Amounts.stringify(amountLeft)}`,
+ );
const withdrawAmount = Amounts.sub(
amountLeft,
refreshedDenom.feeRefresh,
@@ -549,7 +613,7 @@ async function destroyRefreshSession(
function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
return Duration.fromSpec({
- seconds: 5,
+ seconds: 60,
});
}
@@ -772,6 +836,7 @@ async function handleRefreshMeltGone(
"coins",
"denominations",
"coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -796,6 +861,7 @@ async function handleRefreshMeltGone(
refreshSession.lastError = errDetails;
await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
await tx.refreshGroups.put(rg);
+ await ctx.updateTransactionMeta(tx);
await tx.refreshSessions.put(refreshSession);
},
);
@@ -851,6 +917,7 @@ async function handleRefreshMeltConflict(
"denominations",
"coins",
"coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -877,6 +944,7 @@ async function handleRefreshMeltConflict(
}
refreshSession.lastError = errDetails;
await tx.refreshGroups.put(rg);
+ await ctx.updateTransactionMeta(tx);
await tx.refreshSessions.put(refreshSession);
} else {
// Try again with new denoms!
@@ -921,6 +989,7 @@ async function handleRefreshMeltNotFound(
"coins",
"denominations",
"coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -945,6 +1014,7 @@ async function handleRefreshMeltNotFound(
await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
refreshSession.lastError = errDetails;
await tx.refreshGroups.put(rg);
+ await ctx.updateTransactionMeta(tx);
await tx.refreshSessions.put(refreshSession);
},
);
@@ -1211,7 +1281,6 @@ async function refreshReveal(
coinEvHash: pc.coinEvHash,
maxAge: pc.maxAge,
ageCommitmentProof: pc.ageCommitmentProof,
- spendAllocation: undefined,
};
coins.push(coin);
@@ -1226,6 +1295,7 @@ async function refreshReveal(
"coinAvailability",
"refreshGroups",
"refreshSessions",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1274,6 +1344,7 @@ async function refreshReveal(
await tx.coinAvailability.put(car);
}
await tx.refreshGroups.put(rg);
+ await ctx.updateTransactionMeta(tx);
},
);
logger.trace("refresh finished (end of reveal)");
@@ -1292,6 +1363,7 @@ async function handleRefreshRevealError(
"coins",
"denominations",
"coinAvailability",
+ "transactionsMeta",
],
},
async (tx) => {
@@ -1317,6 +1389,7 @@ async function handleRefreshRevealError(
await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
await tx.refreshGroups.put(rg);
await tx.refreshSessions.put(refreshSession);
+ await ctx.updateTransactionMeta(tx);
},
);
}
@@ -1325,6 +1398,10 @@ export async function processRefreshGroup(
wex: WalletExecutionContext,
refreshGroupId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
logger.trace(`processing refresh group ${refreshGroupId}`);
const refreshGroup = await wex.db.runReadOnlyTx(
@@ -1345,35 +1422,36 @@ export async function processRefreshGroup(
throw Error("refresh blocked");
}
- // Process refresh sessions of the group in parallel.
logger.trace(
`processing refresh sessions for ${refreshGroup.oldCoinPubs.length} old coins`,
);
let errors: TalerErrorDetail[] = [];
let inShutdown = false;
- const ps = refreshGroup.oldCoinPubs.map((x, i) =>
- processRefreshSession(wex, refreshGroupId, i).catch((x) => {
+
+ // Process refresh sessions in sequence.
+ // In the future, we could parallelize request, in particular when multiple
+ // exchanges are involved.
+ // But we need to make sure that we write results to DB with high priority,
+ // otherwise we run into problems with very large refresh groups, where we'd first
+ // do many many network requests before even going to the DB.
+
+ for (let i = 0; i < refreshGroup.oldCoinPubs.length; i++) {
+ try {
+ await processRefreshSession(wex, refreshGroupId, i);
+ } catch (x) {
if (x instanceof CryptoApiStoppedError) {
inShutdown = true;
logger.info(
"crypto API stopped while processing refresh group, probably the wallet is currently shutting down.",
);
- return;
- }
- if (x instanceof TalerError) {
- logger.warn("process refresh session got exception (TalerError)");
- logger.warn(`exc ${x}`);
- logger.warn(`exc stack ${x.stack}`);
- logger.warn(`error detail: ${j2s(x.errorDetail)}`);
- } else {
- logger.warn("process refresh session got exception");
- logger.warn(`exc ${x}`);
- logger.warn(`exc stack ${x.stack}`);
+ break;
}
- errors.push(getErrorDetailFromException(x));
- }),
- );
- await Promise.all(ps);
+ const err = getErrorDetailFromException(x);
+ logger.warn(`exception in refresh session: ${j2s(err)}`);
+ errors.push(err);
+ }
+ }
+
if (inShutdown) {
return TaskRunResult.finished();
}
@@ -1384,7 +1462,14 @@ export async function processRefreshGroup(
// status of the whole refresh group.
const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["coins", "coinAvailability", "refreshGroups"] },
+ {
+ storeNames: [
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "transactionsMeta",
+ ],
+ },
async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
@@ -1420,6 +1505,7 @@ export async function processRefreshGroup(
}
await makeCoinsVisible(wex, tx, ctx.transactionId);
await tx.refreshGroups.put(rg);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computeRefreshTransactionState(rg);
return {
oldTxState,
@@ -1547,7 +1633,13 @@ export async function calculateRefreshOutput(
async function applyRefreshToOldCoins(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["denominations", "coins", "refreshGroups", "coinAvailability"]
+ [
+ "denominations",
+ "coins",
+ "coinHistory",
+ "refreshGroups",
+ "coinAvailability",
+ ]
>,
oldCoinPubs: CoinRefreshRequest[],
refreshGroupId: string,
@@ -1605,16 +1697,24 @@ async function applyRefreshToOldCoins(
default:
assertUnreachable(coin.status);
}
- if (!coin.spendAllocation) {
- coin.spendAllocation = {
- amount: Amounts.stringify(ocp.amount),
- // id: `txn:refresh:${refreshGroupId}`,
- id: constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- }),
+ let histEntry: CoinHistoryRecord | undefined = await tx.coinHistory.get(
+ coin.coinPub,
+ );
+ if (!histEntry) {
+ histEntry = {
+ coinPub: coin.coinPub,
+ history: [],
};
}
+ histEntry.history.push({
+ type: "refresh",
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Refresh,
+ refreshGroupId,
+ }),
+ amount: Amounts.stringify(ocp.amount),
+ });
+ await tx.coinHistory.put(histEntry);
await tx.coins.put(coin);
}
}
@@ -1639,9 +1739,11 @@ export async function createRefreshGroup(
[
"denominations",
"coins",
+ "coinHistory",
"refreshGroups",
"refreshSessions",
"coinAvailability",
+ "transactionsMeta",
]
>,
currency: string,
@@ -1658,6 +1760,12 @@ export async function createRefreshGroup(
const estimatedOutputPerCoin = outInfo.outputPerCoin;
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `creating refresh group, inputs ${j2s(oldCoinPubs.map((x) => x.amount))}`,
+ );
+ }
+
await applyRefreshToOldCoins(wex, tx, oldCoinPubs, refreshGroupId);
const refreshGroup: RefreshGroupRecord = {
@@ -1689,14 +1797,15 @@ export async function createRefreshGroup(
await initRefreshSession(wex, tx, refreshGroup, i);
}
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+
await tx.refreshGroups.put(refreshGroup);
+ await ctx.updateTransactionMeta(tx);
const newTxState = computeRefreshTransactionState(refreshGroup);
logger.trace(`created refresh group ${refreshGroupId}`);
- const ctx = new RefreshTransactionContext(wex, refreshGroupId);
-
// Shepherd the task.
// If the current transaction fails to commit the refresh
// group to the DB, the shepherd will give up.
@@ -1796,6 +1905,8 @@ export async function forceRefresh(
"refreshSessions",
"denominations",
"coins",
+ "coinHistory",
+ "transactionsMeta",
],
},
async (tx) => {
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index 470f45aff..c52c55f50 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -53,6 +53,7 @@ import {
OPERATION_STATUS_NONFINAL_FIRST,
OPERATION_STATUS_NONFINAL_LAST,
OperationRetryRecord,
+ ReserveRecordStatus,
WalletDbAllStoresReadOnlyTransaction,
WalletDbReadOnlyTransaction,
timestampAbsoluteFromDb,
@@ -64,6 +65,7 @@ import {
} from "./deposits.js";
import {
computeDenomLossTransactionStatus,
+ processExchangeKyc,
updateExchangeFromUrlHandler,
} from "./exchanges.js";
import {
@@ -127,6 +129,7 @@ function taskGivesLiveness(taskId: string): boolean {
switch (parsedTaskId.tag) {
case PendingTaskType.Backup:
case PendingTaskType.ExchangeUpdate:
+ case PendingTaskType.ExchangeWalletKyc:
return false;
case PendingTaskType.Deposit:
case PendingTaskType.PeerPullCredit:
@@ -145,6 +148,14 @@ function taskGivesLiveness(taskId: string): boolean {
}
export interface TaskScheduler {
+ /**
+ * Ensure that the task scheduler is running.
+ *
+ * If it is not running, start it, with previous
+ * tasks loaded from the database.
+ *
+ * Returns after the scheduler is running.
+ */
ensureRunning(): Promise<void>;
startShepherdTask(taskId: TaskIdStr): void;
stopShepherdTask(taskId: TaskIdStr): void;
@@ -188,6 +199,9 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
}
+ /**
+ * @see TaskScheduler.ensureRunning
+ */
async ensureRunning(): Promise<void> {
if (this.isRunning) {
return;
@@ -261,12 +275,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
const tasksIds = [...this.sheps.keys()];
logger.info(`reloading shepherd with ${tasksIds.length} tasks`);
for (const taskId of tasksIds) {
- await this.stopShepherdTask(taskId);
+ this.stopShepherdTask(taskId);
}
for (const taskId of tasksIds) {
this.startShepherdTask(taskId);
}
}
+
private async internalStartShepherdTask(taskId: TaskIdStr): Promise<void> {
logger.trace(`Starting to shepherd task ${taskId}`);
const oldShep = this.sheps.get(taskId);
@@ -276,9 +291,10 @@ export class TaskSchedulerImpl implements TaskScheduler {
return;
}
logger.trace(
- `Waiting old task to complete the loop in cancel mode ${taskId}`,
+ `Waiting for old task to complete the loop in cancel mode ${taskId}`,
);
await oldShep.latch;
+ logger.trace(`Old task ${taskId} completed in cancel mode`);
}
logger.trace(`Creating new shepherd for ${taskId}`);
const newShep: ShepherdInfo = {
@@ -380,10 +396,13 @@ export class TaskSchedulerImpl implements TaskScheduler {
try {
res = await callOperationHandlerForTaskId(wex, taskId);
} catch (e) {
- logger.trace(`Shepherd error ${taskId} saving response ${e}`);
+ const errorDetail = getErrorDetailFromException(e);
+ logger.trace(
+ `Shepherd error ${taskId} saving response ${j2s(errorDetail)}`,
+ );
res = {
type: TaskRunResultType.Error,
- errorDetail: getErrorDetailFromException(e),
+ errorDetail,
};
}
if (info.cts.token.isCancelled) {
@@ -464,6 +483,14 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
break;
}
+ case TaskRunResultType.NetworkRequired: {
+ logger.trace(`Shepherd for ${taskId} got network-required result.`);
+ await storePendingTaskPending(this.ws, taskId);
+ const delay = Duration.getForever();
+ logger.trace(`Not retrying task until network is restored.`);
+ await this.wait(taskId, info, delay);
+ break;
+ }
default:
assertUnreachable(res);
}
@@ -598,12 +625,17 @@ function getWalletExecutionContextForTask(
},
};
- wex = getObservedWalletExecutionContext(ws, cancellationToken, oc);
+ wex = getObservedWalletExecutionContext(
+ ws,
+ cancellationToken,
+ undefined,
+ oc,
+ );
} else {
oc = {
observe(evt) {},
};
- wex = getNormalWalletExecutionContext(ws, cancellationToken, oc);
+ wex = getNormalWalletExecutionContext(ws, cancellationToken, undefined, oc);
}
return wex;
}
@@ -613,6 +645,15 @@ async function callOperationHandlerForTaskId(
taskId: TaskIdStr,
): Promise<TaskRunResult> {
const pending = parseTaskIdentifier(taskId);
+
+ const txId = convertTaskToTransactionId(taskId);
+ if (txId) {
+ wex.oc.observe({
+ type: ObservabilityEventType.DeclareConcernsTransaction,
+ transactionId: txId,
+ });
+ }
+
switch (pending.tag) {
case PendingTaskType.ExchangeUpdate:
return await updateExchangeFromUrlHandler(wex, pending.exchangeBaseUrl);
@@ -636,6 +677,8 @@ async function callOperationHandlerForTaskId(
return await processPeerPullDebit(wex, pending.peerPullDebitId);
case PendingTaskType.PeerPushCredit:
return await processPeerPushCredit(wex, pending.peerPushCreditId);
+ case PendingTaskType.ExchangeWalletKyc:
+ return await processExchangeKyc(wex, pending.exchangeBaseUrl);
case PendingTaskType.RewardPickup:
throw Error("not supported anymore");
default:
@@ -662,6 +705,7 @@ async function taskToRetryNotification(
switch (parsedTaskId.tag) {
case PendingTaskType.ExchangeUpdate:
+ case PendingTaskType.ExchangeWalletKyc:
return makeExchangeRetryNotification(ws, tx, pendingTaskId, e);
case PendingTaskType.PeerPullCredit:
case PendingTaskType.PeerPullDebit:
@@ -818,8 +862,12 @@ async function makeExchangeRetryNotification(
): Promise<WalletNotification | undefined> {
logger.info("making exchange retry notification");
const parsedTaskId = parseTaskIdentifier(pendingTaskId);
- if (parsedTaskId.tag !== PendingTaskType.ExchangeUpdate) {
- throw Error("invalid task identifier");
+ switch (parsedTaskId.tag) {
+ case PendingTaskType.ExchangeUpdate:
+ case PendingTaskType.ExchangeWalletKyc:
+ break;
+ default:
+ throw Error("invalid task identifier");
}
const rec = await tx.exchanges.get(parsedTaskId.exchangeBaseUrl);
@@ -843,91 +891,6 @@ async function makeExchangeRetryNotification(
return notif;
}
-export function listTaskForTransactionId(transactionId: string): TaskIdStr[] {
- const tid = parseTransactionIdentifier(transactionId);
- if (!tid) {
- throw Error("invalid task ID");
- }
- switch (tid.tag) {
- case TransactionType.Deposit:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Deposit,
- depositGroupId: tid.depositGroupId,
- }),
- ];
- case TransactionType.InternalWithdrawal:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId: tid.withdrawalGroupId,
- }),
- ];
- case TransactionType.Payment:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Purchase,
- proposalId: tid.proposalId,
- }),
- ];
- case TransactionType.PeerPullCredit:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.PeerPullCredit,
- pursePub: tid.pursePub,
- }),
- ];
- case TransactionType.PeerPullDebit:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.PeerPullDebit,
- peerPullDebitId: tid.peerPullDebitId,
- }),
- ];
- case TransactionType.PeerPushCredit:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.PeerPullCredit,
- pursePub: tid.peerPushCreditId,
- }),
- ];
- case TransactionType.PeerPushDebit:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.PeerPullCredit,
- pursePub: tid.pursePub,
- }),
- ];
- case TransactionType.Recoup:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Recoup,
- recoupGroupId: tid.recoupGroupId,
- }),
- ];
- case TransactionType.Refresh:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Refresh,
- refreshGroupId: tid.refreshGroupId,
- }),
- ];
- case TransactionType.Refund:
- return [];
- case TransactionType.Withdrawal:
- return [
- constructTaskIdentifier({
- tag: PendingTaskType.Withdraw,
- withdrawalGroupId: tid.withdrawalGroupId,
- }),
- ];
- case TransactionType.DenomLoss:
- return [];
- default:
- assertUnreachable(tid);
- }
-}
-
/**
* Convert the task ID for a task that processes a transaction int
* the ID for the transaction.
@@ -1008,6 +971,7 @@ export async function getActiveTaskIds(
"peerPushDebit",
"peerPullDebit",
"peerPushCredit",
+ "reserves",
],
},
async (tx) => {
@@ -1141,7 +1105,7 @@ export async function getActiveTaskIds(
}
}
- // exchange update
+ // exchange update and KYC
{
const exchanges = await tx.exchanges.getAll();
@@ -1151,6 +1115,22 @@ export async function getActiveTaskIds(
exchangeBaseUrl: rec.baseUrl,
});
res.taskIds.push(taskIdUpdate);
+
+ const reserveId = rec.currentMergeReserveRowId;
+ if (reserveId == null) {
+ continue;
+ }
+ const reserveRec = await tx.reserves.get(reserveId);
+ if (
+ reserveRec?.status != null &&
+ reserveRec.status != ReserveRecordStatus.Done
+ ) {
+ const taskIdKyc = constructTaskIdentifier({
+ tag: PendingTaskType.ExchangeWalletKyc,
+ exchangeBaseUrl: rec.baseUrl,
+ });
+ res.taskIds.push(taskIdKyc);
+ }
}
}
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
index 899c4a8b2..8e5c3d760 100644
--- a/packages/taler-wallet-core/src/testing.ts
+++ b/packages/taler-wallet-core/src/testing.ts
@@ -58,7 +58,10 @@ import {
import { getBalances } from "./balance.js";
import { genericWaitForState } from "./common.js";
import { createDepositGroup } from "./deposits.js";
-import { fetchFreshExchange } from "./exchanges.js";
+import {
+ acceptExchangeTermsOfService,
+ fetchFreshExchange,
+} from "./exchanges.js";
import {
confirmPay,
preparePayForUri,
@@ -77,7 +80,7 @@ import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
import { getRefreshesForTransaction } from "./refresh.js";
import { getTransactionById, getTransactions } from "./transactions.js";
import type { WalletExecutionContext } from "./wallet.js";
-import { acceptWithdrawalFromUri } from "./withdraw.js";
+import { acceptBankIntegratedWithdrawal } from "./withdraw.js";
const logger = new Logger("operations/testing.ts");
@@ -122,7 +125,10 @@ export async function withdrawTestBalance(
amount,
);
- const acceptResp = await acceptWithdrawalFromUri(wex, {
+ await fetchFreshExchange(wex, req.exchangeBaseUrl);
+ await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
+
+ const acceptResp = await acceptBankIntegratedWithdrawal(wex, {
talerWithdrawUri: wresp.taler_withdraw_uri,
selectedExchange: exchangeBaseUrl,
forcedDenomSel: req.forcedDenomSel,
@@ -410,6 +416,7 @@ export async function waitUntilAllTransactionsFinal(
switch (notif.newTxState.major) {
case TransactionMajorState.Pending:
case TransactionMajorState.Aborting:
+ case TransactionMajorState.Finalizing:
return false;
default:
return true;
@@ -424,6 +431,7 @@ export async function waitUntilAllTransactionsFinal(
switch (tx.txState.major) {
case TransactionMajorState.Pending:
case TransactionMajorState.Aborting:
+ case TransactionMajorState.Finalizing:
case TransactionMajorState.Suspended:
case TransactionMajorState.SuspendedAborting:
logger.info(
@@ -497,6 +505,7 @@ export async function waitUntilGivenTransactionsFinal(
}
switch (tx.txState.major) {
case TransactionMajorState.Pending:
+ case TransactionMajorState.Finalizing:
case TransactionMajorState.Aborting:
case TransactionMajorState.Suspended:
case TransactionMajorState.SuspendedAborting:
@@ -542,6 +551,7 @@ export async function waitUntilRefreshesDone(
}
switch (tx.txState.major) {
case TransactionMajorState.Pending:
+ case TransactionMajorState.Finalizing:
case TransactionMajorState.Aborting:
case TransactionMajorState.Suspended:
case TransactionMajorState.SuspendedAborting:
@@ -573,7 +583,7 @@ async function waitUntilTransactionPendingReady(
export async function waitTransactionState(
wex: WalletExecutionContext,
transactionId: string,
- txState: TransactionState,
+ txState: TransactionState | TransactionState[],
): Promise<void> {
logger.info(
`starting waiting for ${transactionId} to be in ${JSON.stringify(
@@ -585,9 +595,22 @@ export async function waitTransactionState(
const tx = await getTransactionById(wex, {
transactionId,
});
- return (
- tx.txState.major === txState.major && tx.txState.minor === txState.minor
- );
+ if (Array.isArray(txState)) {
+ for (const myState of txState) {
+ if (
+ tx.txState.major === myState.major &&
+ tx.txState.minor === myState.minor
+ ) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return (
+ tx.txState.major === txState.major &&
+ tx.txState.minor === txState.minor
+ );
+ }
},
filterNotification(notif) {
return notif.type === NotificationType.TransactionStateTransition;
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 7782d09ba..3bf0f7d10 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -17,125 +17,50 @@
/**
* Imports.
*/
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { GlobalIDB, IDBKeyRange } from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
Amounts,
assertUnreachable,
- checkDbInvariant,
- DepositTransactionTrackingState,
j2s,
Logger,
+ makeTalerErrorDetail,
NotificationType,
- OrderShortInfo,
- PeerContractTerms,
- RefundInfoShort,
- RefundPaymentInfo,
ScopeType,
- stringifyPayPullUri,
- stringifyPayPushUri,
TalerErrorCode,
- TalerPreciseTimestamp,
Transaction,
- TransactionAction,
TransactionByIdRequest,
TransactionIdStr,
TransactionMajorState,
- TransactionRecordFilter,
TransactionsRequest,
TransactionsResponse,
TransactionState,
TransactionType,
- TransactionWithdrawal,
- WalletContractData,
- WithdrawalTransactionByURIRequest,
- WithdrawalType,
} from "@gnu-taler/taler-util";
import {
constructTaskIdentifier,
PendingTaskType,
- TaskIdentifiers,
TaskIdStr,
TransactionContext,
} from "./common.js";
import {
- DenomLossEventRecord,
- DepositElementStatus,
- DepositGroupRecord,
OPERATION_STATUS_NONFINAL_FIRST,
OPERATION_STATUS_NONFINAL_LAST,
- OperationRetryRecord,
- PeerPullCreditRecord,
- PeerPullDebitRecordStatus,
- PeerPullPaymentIncomingRecord,
- PeerPushCreditStatus,
- PeerPushDebitRecord,
- PeerPushDebitStatus,
- PeerPushPaymentIncomingRecord,
- PurchaseRecord,
- PurchaseStatus,
- RefreshGroupRecord,
- RefreshOperationStatus,
- RefundGroupRecord,
- timestampPreciseFromDb,
- timestampProtocolFromDb,
- WalletDbReadOnlyTransaction,
- WithdrawalGroupRecord,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
+ WalletDbAllStoresReadWriteTransaction,
} from "./db.js";
+import { DepositTransactionContext } from "./deposits.js";
+import { DenomLossTransactionContext } from "./exchanges.js";
import {
- computeDepositTransactionActions,
- computeDepositTransactionStatus,
- DepositTransactionContext,
-} from "./deposits.js";
-import {
- computeDenomLossTransactionStatus,
- DenomLossTransactionContext,
- ExchangeWireDetails,
- getExchangeWireDetailsInTx,
-} from "./exchanges.js";
-import {
- computePayMerchantTransactionActions,
- computePayMerchantTransactionState,
- computeRefundTransactionState,
- expectProposalDownload,
- extractContractData,
PayMerchantTransactionContext,
RefundTransactionContext,
} from "./pay-merchant.js";
-import {
- computePeerPullCreditTransactionActions,
- computePeerPullCreditTransactionState,
- PeerPullCreditTransactionContext,
-} from "./pay-peer-pull-credit.js";
-import {
- computePeerPullDebitTransactionActions,
- computePeerPullDebitTransactionState,
- PeerPullDebitTransactionContext,
-} from "./pay-peer-pull-debit.js";
-import {
- computePeerPushCreditTransactionActions,
- computePeerPushCreditTransactionState,
- PeerPushCreditTransactionContext,
-} from "./pay-peer-push-credit.js";
-import {
- computePeerPushDebitTransactionActions,
- computePeerPushDebitTransactionState,
- PeerPushDebitTransactionContext,
-} from "./pay-peer-push-debit.js";
-import {
- computeRefreshTransactionActions,
- computeRefreshTransactionState,
- RefreshTransactionContext,
-} from "./refresh.js";
+import { PeerPullCreditTransactionContext } from "./pay-peer-pull-credit.js";
+import { PeerPullDebitTransactionContext } from "./pay-peer-pull-debit.js";
+import { PeerPushCreditTransactionContext } from "./pay-peer-push-credit.js";
+import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js";
+import { RefreshTransactionContext } from "./refresh.js";
import type { WalletExecutionContext } from "./wallet.js";
-import {
- augmentPaytoUrisForWithdrawal,
- computeWithdrawalTransactionActions,
- computeWithdrawalTransactionStatus,
- WithdrawTransactionContext,
-} from "./withdraw.js";
+import { WithdrawTransactionContext } from "./withdraw.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
@@ -177,22 +102,6 @@ function shouldSkipCurrency(
return false;
}
-function shouldSkipSearch(
- transactionsRequest: TransactionsRequest | undefined,
- fields: string[],
-): boolean {
- if (!transactionsRequest?.search) {
- return false;
- }
- const needle = transactionsRequest.search.trim();
- for (const f of fields) {
- if (f.indexOf(needle) >= 0) {
- return false;
- }
- }
- return true;
-}
-
/**
* Fallback order of transactions that have the same timestamp.
*/
@@ -223,563 +132,29 @@ export async function getTransactionById(
switch (parsedTx.tag) {
case TransactionType.InternalWithdrawal:
- case TransactionType.Withdrawal: {
- const withdrawalGroupId = parsedTx.withdrawalGroupId;
- return await wex.db.runReadWriteTx(
- {
- storeNames: [
- "withdrawalGroups",
- "exchangeDetails",
- "exchanges",
- "operationRetries",
- ],
- },
- async (tx) => {
- const withdrawalGroupRecord =
- await tx.withdrawalGroups.get(withdrawalGroupId);
-
- if (!withdrawalGroupRecord) throw Error("not found");
-
- const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
- const ort = await tx.operationRetries.get(opId);
-
- const exchangeDetails =
- withdrawalGroupRecord.exchangeBaseUrl === undefined
- ? undefined
- : await getExchangeWireDetailsInTx(
- tx,
- withdrawalGroupRecord.exchangeBaseUrl,
- );
- // if (!exchangeDetails) throw Error("not exchange details");
-
- if (
- withdrawalGroupRecord.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- return buildTransactionForBankIntegratedWithdraw(
- withdrawalGroupRecord,
- exchangeDetails,
- ort,
- );
- }
- checkDbInvariant(
- exchangeDetails !== undefined,
- "manual withdrawal without exchange",
- );
- return buildTransactionForManualWithdraw(
- withdrawalGroupRecord,
- exchangeDetails,
- ort,
- );
- },
- );
- }
-
- case TransactionType.DenomLoss: {
- const rec = await wex.db.runReadOnlyTx(
- { storeNames: ["denomLossEvents"] },
- async (tx) => {
- return tx.denomLossEvents.get(parsedTx.denomLossEventId);
- },
- );
- if (!rec) {
- throw Error("denom loss record not found");
- }
- return buildTransactionForDenomLoss(rec);
- }
-
+ case TransactionType.Withdrawal:
+ case TransactionType.DenomLoss:
case TransactionType.Recoup:
- throw new Error("not yet supported");
-
- case TransactionType.Payment: {
- const proposalId = parsedTx.proposalId;
- return await wex.db.runReadWriteTx(
- {
- storeNames: [
- "purchases",
- "tombstones",
- "operationRetries",
- "contractTerms",
- "refundGroups",
- ],
- },
- async (tx) => {
- const purchase = await tx.purchases.get(proposalId);
- if (!purchase) throw Error("not found");
- const download = await expectProposalDownload(wex, purchase, tx);
- const contractData = download.contractData;
- const payOpId = TaskIdentifiers.forPay(purchase);
- const payRetryRecord = await tx.operationRetries.get(payOpId);
-
- const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
- purchase.proposalId,
- );
-
- return buildTransactionForPurchase(
- purchase,
- contractData,
- refunds,
- payRetryRecord,
- );
- },
- );
- }
-
- case TransactionType.Refresh: {
- // FIXME: We should return info about the refresh here!;
- const refreshGroupId = parsedTx.refreshGroupId;
- return await wex.db.runReadOnlyTx(
- { storeNames: ["refreshGroups", "operationRetries"] },
- async (tx) => {
- const refreshGroupRec = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroupRec) {
- throw Error("not found");
- }
- const retries = await tx.operationRetries.get(
- TaskIdentifiers.forRefresh(refreshGroupRec),
- );
- return buildTransactionForRefresh(refreshGroupRec, retries);
- },
- );
- }
-
- case TransactionType.Deposit: {
- const depositGroupId = parsedTx.depositGroupId;
- return await wex.db.runReadWriteTx(
- { storeNames: ["depositGroups", "operationRetries"] },
- async (tx) => {
- const depositRecord = await tx.depositGroups.get(depositGroupId);
- if (!depositRecord) throw Error("not found");
-
- const retries = await tx.operationRetries.get(
- TaskIdentifiers.forDeposit(depositRecord),
- );
- return buildTransactionForDeposit(depositRecord, retries);
- },
- );
- }
-
+ case TransactionType.PeerPushDebit:
+ case TransactionType.PeerPushCredit:
+ case TransactionType.Refresh:
+ case TransactionType.PeerPullCredit:
+ case TransactionType.Payment:
+ case TransactionType.Deposit:
+ case TransactionType.PeerPullDebit:
case TransactionType.Refund: {
- return await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "refundGroups",
- "purchases",
- "operationRetries",
- "contractTerms",
- ],
- },
- async (tx) => {
- const refundRecord = await tx.refundGroups.get(
- parsedTx.refundGroupId,
- );
- if (!refundRecord) {
- throw Error("not found");
- }
- const contractData = await lookupMaybeContractData(
- tx,
- refundRecord?.proposalId,
- );
- return buildTransactionForRefund(refundRecord, contractData);
- },
- );
- }
- case TransactionType.PeerPullDebit: {
- return await wex.db.runReadWriteTx(
- { storeNames: ["peerPullDebit", "contractTerms"] },
- async (tx) => {
- const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId);
- if (!debit) throw Error("not found");
- const contractTermsRec = await tx.contractTerms.get(
- debit.contractTermsHash,
- );
- if (!contractTermsRec)
- throw Error("contract terms for peer-pull-debit not found");
- return buildTransactionForPullPaymentDebit(
- debit,
- contractTermsRec.contractTermsRaw,
- );
- },
- );
- }
-
- case TransactionType.PeerPushDebit: {
- return await wex.db.runReadWriteTx(
- { storeNames: ["peerPushDebit", "contractTerms"] },
- async (tx) => {
- const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
- if (!debit) throw Error("not found");
- const ct = await tx.contractTerms.get(debit.contractTermsHash);
- checkDbInvariant(
- !!ct,
- `no contract terms for p2p push ${parsedTx.pursePub}`,
- );
- return buildTransactionForPushPaymentDebit(
- debit,
- ct.contractTermsRaw,
- );
- },
- );
- }
-
- case TransactionType.PeerPushCredit: {
- const peerPushCreditId = parsedTx.peerPushCreditId;
- return await wex.db.runReadWriteTx(
- {
- storeNames: [
- "peerPushCredit",
- "contractTerms",
- "withdrawalGroups",
- "operationRetries",
- ],
- },
- async (tx) => {
- const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushInc) throw Error("not found");
- const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(
- !!ct,
- `no contract terms for p2p push ${peerPushCreditId}`,
- );
-
- let wg: WithdrawalGroupRecord | undefined = undefined;
- let wgOrt: OperationRetryRecord | undefined = undefined;
- if (pushInc.withdrawalGroupId) {
- wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
- if (wg) {
- const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
- wgOrt = await tx.operationRetries.get(withdrawalOpId);
- }
- }
- const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
- const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
-
- return buildTransactionForPeerPushCredit(
- pushInc,
- pushIncOrt,
- ct.contractTermsRaw,
- wg,
- wgOrt,
- );
- },
- );
- }
-
- case TransactionType.PeerPullCredit: {
- const pursePub = parsedTx.pursePub;
- return await wex.db.runReadWriteTx(
- {
- storeNames: [
- "peerPullCredit",
- "contractTerms",
- "withdrawalGroups",
- "operationRetries",
- ],
- },
- async (tx) => {
- const pushInc = await tx.peerPullCredit.get(pursePub);
- if (!pushInc) throw Error("not found");
- const ct = await tx.contractTerms.get(pushInc.contractTermsHash);
- checkDbInvariant(!!ct, `no contract terms for p2p push ${pursePub}`);
-
- let wg: WithdrawalGroupRecord | undefined = undefined;
- let wgOrt: OperationRetryRecord | undefined = undefined;
- if (pushInc.withdrawalGroupId) {
- wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
- if (wg) {
- const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
- wgOrt = await tx.operationRetries.get(withdrawalOpId);
- }
- }
- const pushIncOpId =
- TaskIdentifiers.forPeerPullPaymentInitiation(pushInc);
- let pushIncOrt = await tx.operationRetries.get(pushIncOpId);
-
- return buildTransactionForPeerPullCredit(
- pushInc,
- pushIncOrt,
- ct.contractTermsRaw,
- wg,
- wgOrt,
- );
- },
+ const ctx = await getContextForTransaction(wex, req.transactionId);
+ const txDetails = await wex.db.runAllStoresReadOnlyTx({}, async (tx) =>
+ ctx.lookupFullTransaction(tx),
);
+ if (!txDetails) {
+ throw Error("transaction not found");
+ }
+ return txDetails;
}
}
}
-function buildTransactionForPushPaymentDebit(
- pi: PeerPushDebitRecord,
- contractTerms: PeerContractTerms,
- ort?: OperationRetryRecord,
-): Transaction {
- let talerUri: string | undefined = undefined;
- switch (pi.status) {
- case PeerPushDebitStatus.PendingReady:
- case PeerPushDebitStatus.SuspendedReady:
- talerUri = stringifyPayPushUri({
- exchangeBaseUrl: pi.exchangeBaseUrl,
- contractPriv: pi.contractPriv,
- });
- }
- const txState = computePeerPushDebitTransactionState(pi);
- return {
- type: TransactionType.PeerPushDebit,
- txState,
- txActions: computePeerPushDebitTransactionActions(pi),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(pi.totalCost))
- : pi.totalCost,
- amountRaw: pi.amount,
- exchangeBaseUrl: pi.exchangeBaseUrl,
- info: {
- expiration: contractTerms.purse_expiration,
- summary: contractTerms.summary,
- },
- timestamp: timestampPreciseFromDb(pi.timestampCreated),
- talerUri,
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: pi.pursePub,
- }),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForPullPaymentDebit(
- pi: PeerPullPaymentIncomingRecord,
- contractTerms: PeerContractTerms,
- ort?: OperationRetryRecord,
-): Transaction {
- const txState = computePeerPullDebitTransactionState(pi);
- return {
- type: TransactionType.PeerPullDebit,
- txState,
- txActions: computePeerPullDebitTransactionActions(pi),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(pi.amount))
- : pi.coinSel?.totalCost
- ? pi.coinSel?.totalCost
- : Amounts.stringify(pi.amount),
- amountRaw: Amounts.stringify(pi.amount),
- exchangeBaseUrl: pi.exchangeBaseUrl,
- info: {
- expiration: contractTerms.purse_expiration,
- summary: contractTerms.summary,
- },
- timestamp: timestampPreciseFromDb(pi.timestampCreated),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId: pi.peerPullDebitId,
- }),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForPeerPullCredit(
- pullCredit: PeerPullCreditRecord,
- pullCreditOrt: OperationRetryRecord | undefined,
- peerContractTerms: PeerContractTerms,
- wsr: WithdrawalGroupRecord | undefined,
- wsrOrt: OperationRetryRecord | undefined,
-): Transaction {
- if (wsr) {
- if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
- throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
- }
- /**
- * FIXME: this should be handled in the withdrawal process.
- * PeerPull withdrawal fails until reserve have funds but it is not
- * an error from the user perspective.
- */
- const silentWithdrawalErrorForInvoice =
- wsrOrt?.lastError &&
- wsrOrt.lastError.code ===
- TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
- Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
- return (
- e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
- e.httpStatusCode === 409
- );
- });
- const txState = computePeerPullCreditTransactionState(pullCredit);
- checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
- checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
- checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
- return {
- type: TransactionType.PeerPullCredit,
- txState,
- txActions: computePeerPullCreditTransactionActions(pullCredit),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount))
- : Amounts.stringify(wsr.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wsr.instructedAmount),
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
- info: {
- expiration: peerContractTerms.purse_expiration,
- summary: peerContractTerms.summary,
- },
- talerUri: stringifyPayPullUri({
- exchangeBaseUrl: wsr.exchangeBaseUrl,
- contractPriv: wsr.wgInfo.contractPriv,
- }),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullCredit.pursePub,
- }),
- kycUrl: pullCredit.kycUrl,
- ...(wsrOrt?.lastError
- ? {
- error: silentWithdrawalErrorForInvoice
- ? undefined
- : wsrOrt.lastError,
- }
- : {}),
- };
- }
-
- const txState = computePeerPullCreditTransactionState(pullCredit);
- return {
- type: TransactionType.PeerPullCredit,
- txState,
- txActions: computePeerPullCreditTransactionActions(pullCredit),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
- : Amounts.stringify(pullCredit.estimatedAmountEffective),
- amountRaw: Amounts.stringify(peerContractTerms.amount),
- exchangeBaseUrl: pullCredit.exchangeBaseUrl,
- timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
- info: {
- expiration: peerContractTerms.purse_expiration,
- summary: peerContractTerms.summary,
- },
- talerUri: stringifyPayPullUri({
- exchangeBaseUrl: pullCredit.exchangeBaseUrl,
- contractPriv: pullCredit.contractPriv,
- }),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullCredit,
- pursePub: pullCredit.pursePub,
- }),
- kycUrl: pullCredit.kycUrl,
- ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
- };
-}
-
-function buildTransactionForPeerPushCredit(
- pushInc: PeerPushPaymentIncomingRecord,
- pushOrt: OperationRetryRecord | undefined,
- peerContractTerms: PeerContractTerms,
- wg: WithdrawalGroupRecord | undefined,
- wsrOrt: OperationRetryRecord | undefined,
-): Transaction {
- if (wg) {
- if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
- throw Error("invalid withdrawal group type for push payment credit");
- }
- checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
- checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
- checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
-
- const txState = computePeerPushCreditTransactionState(pushInc);
- return {
- type: TransactionType.PeerPushCredit,
- txState,
- txActions: computePeerPushCreditTransactionActions(pushInc),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount))
- : Amounts.stringify(wg.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wg.instructedAmount),
- exchangeBaseUrl: wg.exchangeBaseUrl,
- info: {
- expiration: peerContractTerms.purse_expiration,
- summary: peerContractTerms.summary,
- },
- timestamp: timestampPreciseFromDb(wg.timestampStart),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: pushInc.peerPushCreditId,
- }),
- kycUrl: pushInc.kycUrl,
- ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}),
- };
- }
-
- const txState = computePeerPushCreditTransactionState(pushInc);
- return {
- type: TransactionType.PeerPushCredit,
- txState,
- txActions: computePeerPushCreditTransactionActions(pushInc),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
- : // FIXME: This is wrong, needs to consider fees!
- Amounts.stringify(peerContractTerms.amount),
- amountRaw: Amounts.stringify(peerContractTerms.amount),
- exchangeBaseUrl: pushInc.exchangeBaseUrl,
- info: {
- expiration: peerContractTerms.purse_expiration,
- summary: peerContractTerms.summary,
- },
- kycUrl: pushInc.kycUrl,
- timestamp: timestampPreciseFromDb(pushInc.timestamp),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushCredit,
- peerPushCreditId: pushInc.peerPushCreditId,
- }),
- ...(pushOrt?.lastError ? { error: pushOrt.lastError } : {}),
- };
-}
-
-function buildTransactionForBankIntegratedWithdraw(
- wg: WithdrawalGroupRecord,
- exchangeDetails: ExchangeWireDetails | undefined,
- ort?: OperationRetryRecord,
-): TransactionWithdrawal {
- if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
- throw Error("");
- }
- const instructedCurrency =
- wg.instructedAmount === undefined
- ? undefined
- : Amounts.currencyOf(wg.instructedAmount);
- const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
- checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
- const txState = computeWithdrawalTransactionStatus(wg);
-
- const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
- return {
- type: TransactionType.Withdrawal,
- txState,
- txActions: computeWithdrawalTransactionActions(wg),
- exchangeBaseUrl: wg.exchangeBaseUrl,
- amountEffective:
- isUnsuccessfulTransaction(txState) || !wg.denomsSel
- ? zero
- : Amounts.stringify(wg.denomsSel.totalCoinValue),
- amountRaw: !wg.instructedAmount
- ? zero
- : Amounts.stringify(wg.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
- exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
- reservePub: wg.reservePub,
- bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl,
- reserveIsReady:
- wg.status === WithdrawalGroupStatus.Done ||
- wg.status === WithdrawalGroupStatus.PendingReady,
- },
- kycUrl: wg.kycUrl,
- timestamp: timestampPreciseFromDb(wg.timestampStart),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: wg.withdrawalGroupId,
- }),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
export function isUnsuccessfulTransaction(state: TransactionState): boolean {
return (
state.major === TransactionMajorState.Aborted ||
@@ -790,365 +165,6 @@ export function isUnsuccessfulTransaction(state: TransactionState): boolean {
);
}
-function buildTransactionForManualWithdraw(
- wg: WithdrawalGroupRecord,
- exchangeDetails: ExchangeWireDetails,
- ort?: OperationRetryRecord,
-): TransactionWithdrawal {
- if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
- throw Error("");
-
- const plainPaytoUris =
- exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
-
- checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
- checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
- checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
- const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
- plainPaytoUris,
- wg.reservePub,
- wg.instructedAmount,
- );
-
- const txState = computeWithdrawalTransactionStatus(wg);
-
- return {
- type: TransactionType.Withdrawal,
- txState,
- txActions: computeWithdrawalTransactionActions(wg),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount))
- : Amounts.stringify(wg.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wg.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.ManualTransfer,
- reservePub: wg.reservePub,
- exchangePaytoUris,
- exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
- reserveIsReady:
- wg.status === WithdrawalGroupStatus.Done ||
- wg.status === WithdrawalGroupStatus.PendingReady,
- },
- kycUrl: wg.kycUrl,
- exchangeBaseUrl: wg.exchangeBaseUrl,
- timestamp: timestampPreciseFromDb(wg.timestampStart),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: wg.withdrawalGroupId,
- }),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForRefund(
- refundRecord: RefundGroupRecord,
- maybeContractData: WalletContractData | undefined,
-): Transaction {
- let paymentInfo: RefundPaymentInfo | undefined = undefined;
-
- if (maybeContractData) {
- paymentInfo = {
- merchant: maybeContractData.merchant,
- summary: maybeContractData.summary,
- summary_i18n: maybeContractData.summaryI18n,
- };
- }
-
- const txState = computeRefundTransactionState(refundRecord);
- return {
- type: TransactionType.Refund,
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(refundRecord.amountEffective))
- : refundRecord.amountEffective,
- amountRaw: refundRecord.amountRaw,
- refundedTransactionId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: refundRecord.proposalId,
- }),
- timestamp: timestampPreciseFromDb(refundRecord.timestampCreated),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Refund,
- refundGroupId: refundRecord.refundGroupId,
- }),
- txState,
- txActions: [],
- paymentInfo,
- };
-}
-
-function buildTransactionForRefresh(
- refreshGroupRecord: RefreshGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- const inputAmount = Amounts.sumOrZero(
- refreshGroupRecord.currency,
- refreshGroupRecord.inputPerCoin,
- ).amount;
- const outputAmount = Amounts.sumOrZero(
- refreshGroupRecord.currency,
- refreshGroupRecord.expectedOutputPerCoin,
- ).amount;
- const txState = computeRefreshTransactionState(refreshGroupRecord);
- return {
- type: TransactionType.Refresh,
- txState,
- txActions: computeRefreshTransactionActions(refreshGroupRecord),
- refreshReason: refreshGroupRecord.reason,
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(inputAmount))
- : Amounts.stringify(Amounts.sub(outputAmount, inputAmount).amount),
- amountRaw: Amounts.stringify(
- Amounts.zeroOfCurrency(refreshGroupRecord.currency),
- ),
- refreshInputAmount: Amounts.stringify(inputAmount),
- refreshOutputAmount: Amounts.stringify(outputAmount),
- originatingTransactionId: refreshGroupRecord.originatingTransactionId,
- timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId: refreshGroupRecord.refreshGroupId,
- }),
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction {
- const txState = computeDenomLossTransactionStatus(rec);
- return {
- type: TransactionType.DenomLoss,
- txState,
- txActions: [TransactionAction.Delete],
- amountRaw: Amounts.stringify(rec.amount),
- amountEffective: Amounts.stringify(rec.amount),
- timestamp: timestampPreciseFromDb(rec.timestampCreated),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.DenomLoss,
- denomLossEventId: rec.denomLossEventId,
- }),
- lossEventType: rec.eventType,
- exchangeBaseUrl: rec.exchangeBaseUrl,
- };
-}
-
-function buildTransactionForDeposit(
- dg: DepositGroupRecord,
- ort?: OperationRetryRecord,
-): Transaction {
- let deposited = true;
- if (dg.statusPerCoin) {
- for (const d of dg.statusPerCoin) {
- if (d == DepositElementStatus.DepositPending) {
- deposited = false;
- }
- }
- } else {
- deposited = false;
- }
-
- const trackingState: DepositTransactionTrackingState[] = [];
-
- for (const ts of Object.values(dg.trackingState ?? {})) {
- trackingState.push({
- amountRaw: ts.amountRaw,
- timestampExecuted: timestampProtocolFromDb(ts.timestampExecuted),
- wireFee: ts.wireFee,
- wireTransferId: ts.wireTransferId,
- });
- }
-
- let wireTransferProgress = 0;
- if (dg.statusPerCoin) {
- wireTransferProgress =
- (100 *
- dg.statusPerCoin.reduce(
- (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
- 0,
- )) /
- dg.statusPerCoin.length;
- }
-
- const txState = computeDepositTransactionStatus(dg);
- return {
- type: TransactionType.Deposit,
- txState,
- txActions: computeDepositTransactionActions(dg),
- amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost))
- : Amounts.stringify(dg.totalPayCost),
- timestamp: timestampPreciseFromDb(dg.timestampCreated),
- targetPaytoUri: dg.wire.payto_uri,
- wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Deposit,
- depositGroupId: dg.depositGroupId,
- }),
- wireTransferProgress,
- depositGroupId: dg.depositGroupId,
- trackingState,
- deposited,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-async function lookupMaybeContractData(
- tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>,
- proposalId: string,
-): Promise<WalletContractData | undefined> {
- let contractData: WalletContractData | undefined = undefined;
- const purchaseTx = await tx.purchases.get(proposalId);
- if (purchaseTx && purchaseTx.download) {
- const download = purchaseTx.download;
- const contractTermsRecord = await tx.contractTerms.get(
- download.contractTermsHash,
- );
- if (!contractTermsRecord) {
- return;
- }
- contractData = extractContractData(
- contractTermsRecord?.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- );
- }
-
- return contractData;
-}
-
-function buildTransactionForPurchase(
- purchaseRecord: PurchaseRecord,
- contractData: WalletContractData,
- refundsInfo: RefundGroupRecord[],
- ort?: OperationRetryRecord,
-): Transaction {
- const zero = Amounts.zeroOfAmount(contractData.amount);
-
- const info: OrderShortInfo = {
- merchant: {
- name: contractData.merchant.name,
- address: contractData.merchant.address,
- email: contractData.merchant.email,
- jurisdiction: contractData.merchant.jurisdiction,
- website: contractData.merchant.website,
- },
- orderId: contractData.orderId,
- summary: contractData.summary,
- summary_i18n: contractData.summaryI18n,
- contractTermsHash: contractData.contractTermsHash,
- };
-
- if (contractData.fulfillmentUrl !== "") {
- info.fulfillmentUrl = contractData.fulfillmentUrl;
- }
-
- const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
- amountEffective: r.amountEffective,
- amountRaw: r.amountRaw,
- timestamp: TalerPreciseTimestamp.round(
- timestampPreciseFromDb(r.timestampCreated),
- ),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Refund,
- refundGroupId: r.refundGroupId,
- }),
- }));
-
- const timestamp = purchaseRecord.timestampAccept;
- checkDbInvariant(
- !!timestamp,
- `purchase ${purchaseRecord.orderId} without accepted time`,
- );
- checkDbInvariant(
- !!purchaseRecord.payInfo,
- `purchase ${purchaseRecord.orderId} without payinfo`,
- );
-
- const txState = computePayMerchantTransactionState(purchaseRecord);
- return {
- type: TransactionType.Payment,
- txState,
- txActions: computePayMerchantTransactionActions(purchaseRecord),
- amountRaw: Amounts.stringify(contractData.amount),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(zero)
- : Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
- totalRefundRaw: Amounts.stringify(zero), // FIXME!
- totalRefundEffective: Amounts.stringify(zero), // FIXME!
- refundPending:
- purchaseRecord.refundAmountAwaiting === undefined
- ? undefined
- : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
- refunds,
- posConfirmation: purchaseRecord.posConfirmation,
- timestamp: timestampPreciseFromDb(timestamp),
- transactionId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: purchaseRecord.proposalId,
- }),
- proposalId: purchaseRecord.proposalId,
- info,
- refundQueryActive:
- purchaseRecord.purchaseStatus === PurchaseStatus.PendingQueryingRefund,
- ...(ort?.lastError ? { error: ort.lastError } : {}),
- };
-}
-
-export async function getWithdrawalTransactionByUri(
- wex: WalletExecutionContext,
- request: WithdrawalTransactionByURIRequest,
-): Promise<TransactionWithdrawal | undefined> {
- return await wex.db.runReadWriteTx(
- {
- storeNames: [
- "withdrawalGroups",
- "exchangeDetails",
- "exchanges",
- "operationRetries",
- ],
- },
- async (tx) => {
- const withdrawalGroupRecord =
- await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
- request.talerWithdrawUri,
- );
-
- if (!withdrawalGroupRecord) {
- return undefined;
- }
- if (withdrawalGroupRecord.exchangeBaseUrl === undefined) {
- // prepared and unconfirmed withdrawals are hidden
- return undefined;
- }
-
- const opId = TaskIdentifiers.forWithdrawal(withdrawalGroupRecord);
- const ort = await tx.operationRetries.get(opId);
-
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- withdrawalGroupRecord.exchangeBaseUrl,
- );
- if (!exchangeDetails) throw Error("not exchange details");
-
- if (
- withdrawalGroupRecord.wgInfo.withdrawalType ===
- WithdrawalRecordType.BankIntegrated
- ) {
- return buildTransactionForBankIntegratedWithdraw(
- withdrawalGroupRecord,
- exchangeDetails,
- ort,
- );
- }
-
- return buildTransactionForManualWithdraw(
- withdrawalGroupRecord,
- exchangeDetails,
- ort,
- );
- },
- );
-}
-
/**
* Retrieve the full event history for this wallet.
*/
@@ -1158,404 +174,45 @@ export async function getTransactions(
): Promise<TransactionsResponse> {
const transactions: Transaction[] = [];
- const filter: TransactionRecordFilter = {};
- if (transactionsRequest?.filterByState) {
- filter.onlyState = transactionsRequest.filterByState;
- }
-
- await wex.db.runReadOnlyTx(
- {
- storeNames: [
- "coins",
- "denominations",
- "depositGroups",
- "exchangeDetails",
- "exchanges",
- "operationRetries",
- "peerPullDebit",
- "peerPushDebit",
- "peerPushCredit",
- "peerPullCredit",
- "planchets",
- "purchases",
- "contractTerms",
- "recoupGroups",
- "rewards",
- "tombstones",
- "withdrawalGroups",
- "refreshGroups",
- "refundGroups",
- "denomLossEvents",
- ],
- },
- async (tx) => {
- await iterRecordsForPeerPushDebit(tx, filter, async (pi) => {
- const amount = Amounts.parseOrThrow(pi.amount);
- const exchangesInTx = [pi.exchangeBaseUrl];
- if (
- shouldSkipCurrency(
- transactionsRequest,
- amount.currency,
- exchangesInTx,
- )
- ) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- const ct = await tx.contractTerms.get(pi.contractTermsHash);
- checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
- transactions.push(
- buildTransactionForPushPaymentDebit(pi, ct.contractTermsRaw),
- );
- });
-
- await iterRecordsForPeerPullDebit(tx, filter, async (pi) => {
- const amount = Amounts.parseOrThrow(pi.amount);
- const exchangesInTx = [pi.exchangeBaseUrl];
- if (
- shouldSkipCurrency(
- transactionsRequest,
- amount.currency,
- exchangesInTx,
- )
- ) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- if (
- pi.status !== PeerPullDebitRecordStatus.PendingDeposit &&
- pi.status !== PeerPullDebitRecordStatus.Done
- ) {
- // FIXME: Why?!
- return;
- }
-
- const contractTermsRec = await tx.contractTerms.get(
- pi.contractTermsHash,
- );
- if (!contractTermsRec) {
- return;
- }
-
- transactions.push(
- buildTransactionForPullPaymentDebit(
- pi,
- contractTermsRec.contractTermsRaw,
- ),
- );
- });
-
- await iterRecordsForPeerPushCredit(tx, filter, async (pi) => {
- if (!pi.currency) {
- // Legacy transaction
- return;
- }
- const exchangesInTx = [pi.exchangeBaseUrl];
- if (
- shouldSkipCurrency(transactionsRequest, pi.currency, exchangesInTx)
- ) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- if (pi.status === PeerPushCreditStatus.DialogProposed) {
- // We don't report proposed push credit transactions, user needs
- // to scan URI again and confirm to see it.
- return;
- }
- const ct = await tx.contractTerms.get(pi.contractTermsHash);
- let wg: WithdrawalGroupRecord | undefined = undefined;
- let wgOrt: OperationRetryRecord | undefined = undefined;
- if (pi.withdrawalGroupId) {
- wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
- if (wg) {
- const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
- wgOrt = await tx.operationRetries.get(withdrawalOpId);
- }
- }
- const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pi);
- const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
-
- checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
- transactions.push(
- buildTransactionForPeerPushCredit(
- pi,
- pushIncOrt,
- ct.contractTermsRaw,
- wg,
- wgOrt,
- ),
- );
- });
-
- await iterRecordsForPeerPullCredit(tx, filter, async (pi) => {
- const currency = Amounts.currencyOf(pi.amount);
- const exchangesInTx = [pi.exchangeBaseUrl];
- if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) {
- return;
- }
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
- const ct = await tx.contractTerms.get(pi.contractTermsHash);
- let wg: WithdrawalGroupRecord | undefined = undefined;
- let wgOrt: OperationRetryRecord | undefined = undefined;
- if (pi.withdrawalGroupId) {
- wg = await tx.withdrawalGroups.get(pi.withdrawalGroupId);
- if (wg) {
- const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
- wgOrt = await tx.operationRetries.get(withdrawalOpId);
- }
- }
- const pushIncOpId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
- const pushIncOrt = await tx.operationRetries.get(pushIncOpId);
-
- checkDbInvariant(!!ct, `no contract terms for p2p push ${pi.pursePub}`);
- transactions.push(
- buildTransactionForPeerPullCredit(
- pi,
- pushIncOrt,
- ct.contractTermsRaw,
- wg,
- wgOrt,
- ),
- );
- });
-
- await iterRecordsForRefund(tx, filter, async (refundGroup) => {
- const currency = Amounts.currencyOf(refundGroup.amountRaw);
-
- const exchangesInTx: string[] = [];
- const p = await tx.purchases.get(refundGroup.proposalId);
- if (!p || !p.payInfo || !p.payInfo.payCoinSelection) {
- //refund with no payment
- return;
- }
-
- // FIXME: This is very slow, should become obsolete with materialized transactions.
- for (const cp of p.payInfo.payCoinSelection.coinPubs) {
- const c = await tx.coins.get(cp);
- if (c?.exchangeBaseUrl) {
- exchangesInTx.push(c.exchangeBaseUrl);
- }
- }
-
- if (shouldSkipCurrency(transactionsRequest, currency, exchangesInTx)) {
- return;
- }
- const contractData = await lookupMaybeContractData(
- tx,
- refundGroup.proposalId,
- );
- transactions.push(buildTransactionForRefund(refundGroup, contractData));
- });
+ let keyRange: IDBKeyRange | undefined = undefined;
- await iterRecordsForRefresh(tx, filter, async (rg) => {
- const exchangesInTx = rg.infoPerExchange
- ? Object.keys(rg.infoPerExchange)
- : [];
- if (
- shouldSkipCurrency(transactionsRequest, rg.currency, exchangesInTx)
- ) {
- return;
- }
- let required = false;
- const opId = TaskIdentifiers.forRefresh(rg);
- if (transactionsRequest?.includeRefreshes) {
- required = true;
- } else if (rg.operationStatus !== RefreshOperationStatus.Finished) {
- const ort = await tx.operationRetries.get(opId);
- if (ort) {
- required = true;
- }
- }
- if (required) {
- const ort = await tx.operationRetries.get(opId);
- transactions.push(buildTransactionForRefresh(rg, ort));
- }
- });
-
- await iterRecordsForWithdrawal(tx, filter, async (wsr) => {
- if (
- wsr.rawWithdrawalAmount === undefined ||
- wsr.exchangeBaseUrl == undefined
- ) {
- // skip prepared withdrawals which has not been confirmed
- return;
- }
- const exchangesInTx = [wsr.exchangeBaseUrl];
- if (
- shouldSkipCurrency(
- transactionsRequest,
- Amounts.currencyOf(wsr.rawWithdrawalAmount),
- exchangesInTx,
- )
- ) {
- return;
- }
-
- if (shouldSkipSearch(transactionsRequest, [])) {
- return;
- }
-
- const opId = TaskIdentifiers.forWithdrawal(wsr);
- const ort = await tx.operationRetries.get(opId);
-
- switch (wsr.wgInfo.withdrawalType) {
- case WithdrawalRecordType.PeerPullCredit:
- // Will be reported by the corresponding p2p transaction.
- // FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
- // FIXME: Still report if requested with verbose option?
- return;
- case WithdrawalRecordType.PeerPushCredit:
- // Will be reported by the corresponding p2p transaction.
- // FIXME: If this is an orphan withdrawal, still report it as a withdrawal!
- // FIXME: Still report if requested with verbose option?
- return;
- case WithdrawalRecordType.BankIntegrated: {
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- wsr.exchangeBaseUrl,
- );
- if (!exchangeDetails) {
- // FIXME: report somehow
- return;
- }
-
- transactions.push(
- buildTransactionForBankIntegratedWithdraw(
- wsr,
- exchangeDetails,
- ort,
- ),
- );
- return;
- }
-
- case WithdrawalRecordType.BankManual: {
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- wsr.exchangeBaseUrl,
- );
- if (!exchangeDetails) {
- // FIXME: report somehow
- return;
- }
- transactions.push(
- buildTransactionForManualWithdraw(wsr, exchangeDetails, ort),
- );
- return;
- }
- case WithdrawalRecordType.Recoup:
- // FIXME: Do we also report a transaction here?
- return;
- }
- });
-
- await iterRecordsForDenomLoss(tx, filter, async (rec) => {
- const amount = Amounts.parseOrThrow(rec.amount);
- const exchangesInTx = [rec.exchangeBaseUrl];
- if (
- shouldSkipCurrency(
- transactionsRequest,
- amount.currency,
- exchangesInTx,
- )
- ) {
- return;
- }
- transactions.push(buildTransactionForDenomLoss(rec));
- });
-
- await iterRecordsForDeposit(tx, filter, async (dg) => {
- const amount = Amounts.parseOrThrow(dg.amount);
- const exchangesInTx = dg.infoPerExchange
- ? Object.keys(dg.infoPerExchange)
- : [];
- if (
- shouldSkipCurrency(
- transactionsRequest,
- amount.currency,
- exchangesInTx,
- )
- ) {
- return;
- }
- const opId = TaskIdentifiers.forDeposit(dg);
- const retryRecord = await tx.operationRetries.get(opId);
-
- transactions.push(buildTransactionForDeposit(dg, retryRecord));
- });
-
- await iterRecordsForPurchase(tx, filter, async (purchase) => {
- const download = purchase.download;
- if (!download) {
- return;
- }
- if (!purchase.payInfo) {
- return;
- }
-
- const exchangesInTx: string[] = [];
- for (const cp of purchase.payInfo.payCoinSelection?.coinPubs ?? []) {
- const c = await tx.coins.get(cp);
- if (c?.exchangeBaseUrl) {
- exchangesInTx.push(c.exchangeBaseUrl);
- }
- }
-
- if (
- shouldSkipCurrency(
- transactionsRequest,
- download.currency,
- exchangesInTx,
- )
- ) {
- return;
- }
- const contractTermsRecord = await tx.contractTerms.get(
- download.contractTermsHash,
- );
- if (!contractTermsRecord) {
- return;
- }
- if (
- shouldSkipSearch(transactionsRequest, [
- contractTermsRecord?.contractTermsRaw?.summary || "",
- ])
- ) {
- return;
- }
-
- const contractData = extractContractData(
- contractTermsRecord?.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- );
+ if (transactionsRequest?.filterByState === "nonfinal") {
+ keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_NONFINAL_FIRST,
+ OPERATION_STATUS_NONFINAL_LAST,
+ );
+ }
- const payOpId = TaskIdentifiers.forPay(purchase);
- const payRetryRecord = await tx.operationRetries.get(payOpId);
+ await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
+ const allMetaTransactions =
+ await tx.transactionsMeta.indexes.byStatus.getAll(keyRange);
+ for (const metaTx of allMetaTransactions) {
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ metaTx.currency,
+ metaTx.exchanges,
+ )
+ ) {
+ continue;
+ }
- const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
- purchase.proposalId,
- );
+ const parsedTx = parseTransactionIdentifier(metaTx.transactionId);
+ if (
+ parsedTx?.tag === TransactionType.Refresh &&
+ !transactionsRequest?.includeRefreshes
+ ) {
+ continue;
+ }
- transactions.push(
- buildTransactionForPurchase(
- purchase,
- contractData,
- refunds,
- payRetryRecord,
- ),
- );
- });
- },
- );
+ const ctx = await getContextForTransaction(wex, metaTx.transactionId);
+ const txDetails = await ctx.lookupFullTransaction(tx);
+ if (!txDetails) {
+ continue;
+ }
+ transactions.push(txDetails);
+ }
+ });
// One-off checks, because of a bug where the wallet previously
// did not migrate the DB correctly and caused these amounts
@@ -1611,6 +268,73 @@ export async function getTransactions(
return { transactions: [...txPending, ...txNotPending] };
}
+/**
+ * Re-create materialized transactions from scratch.
+ *
+ * Used for migrations.
+ */
+export async function rematerializeTransactions(
+ wex: WalletExecutionContext,
+ tx: WalletDbAllStoresReadWriteTransaction,
+): Promise<void> {
+ logger.info("re-materializing transactions");
+
+ const allTxMeta = await tx.transactionsMeta.getAll();
+ for (const txMeta of allTxMeta) {
+ await tx.transactionsMeta.delete(txMeta.transactionId);
+ }
+
+ await tx.peerPushDebit.iter().forEachAsync(async (x) => {
+ const ctx = new PeerPushDebitTransactionContext(wex, x.pursePub);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.peerPushCredit.iter().forEachAsync(async (x) => {
+ const ctx = new PeerPushCreditTransactionContext(wex, x.peerPushCreditId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.peerPullCredit.iter().forEachAsync(async (x) => {
+ const ctx = new PeerPullCreditTransactionContext(wex, x.pursePub);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.peerPullDebit.iter().forEachAsync(async (x) => {
+ const ctx = new PeerPullDebitTransactionContext(wex, x.peerPullDebitId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.refundGroups.iter().forEachAsync(async (x) => {
+ const ctx = new RefundTransactionContext(wex, x.refundGroupId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.refreshGroups.iter().forEachAsync(async (x) => {
+ const ctx = new RefreshTransactionContext(wex, x.refreshGroupId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.withdrawalGroups.iter().forEachAsync(async (x) => {
+ const ctx = new WithdrawTransactionContext(wex, x.withdrawalGroupId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.denomLossEvents.iter().forEachAsync(async (x) => {
+ const ctx = new DenomLossTransactionContext(wex, x.denomLossEventId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.depositGroups.iter().forEachAsync(async (x) => {
+ const ctx = new DepositTransactionContext(wex, x.depositGroupId);
+ await ctx.updateTransactionMeta(tx);
+ });
+
+ await tx.purchases.iter().forEachAsync(async (x) => {
+ const ctx = new PayMerchantTransactionContext(wex, x.proposalId);
+ await ctx.updateTransactionMeta(tx);
+ });
+}
+
export type ParsedTransactionIdentifier =
| { tag: TransactionType.Deposit; depositGroupId: string }
| { tag: TransactionType.Payment; proposalId: string }
@@ -1812,6 +536,13 @@ export async function retryAll(wex: WalletExecutionContext): Promise<void> {
}
}
+/**
+ * Restart all the running tasks.
+ */
+export async function restartAll(wex: WalletExecutionContext): Promise<void> {
+ await wex.taskScheduler.reload();
+}
+
async function getContextForTransaction(
wex: WalletExecutionContext,
transactionId: string,
@@ -1869,7 +600,12 @@ export async function failTransaction(
transactionId: string,
): Promise<void> {
const ctx = await getContextForTransaction(wex, transactionId);
- await ctx.failTransaction();
+ await ctx.failTransaction(
+ makeTalerErrorDetail(
+ TalerErrorCode.WALLET_TRANSACTION_ABANDONED_BY_USER,
+ {},
+ ),
+ );
}
/**
@@ -1902,7 +638,9 @@ export async function abortTransaction(
transactionId: string,
): Promise<void> {
const ctx = await getContextForTransaction(wex, transactionId);
- await ctx.abortTransaction();
+ await ctx.abortTransaction(
+ makeTalerErrorDetail(TalerErrorCode.WALLET_TRANSACTION_ABORTED_BY_USER, {}),
+ );
}
export interface TransitionInfo {
@@ -1933,189 +671,16 @@ export function notifyTransition(
transactionId,
experimentalUserData,
});
- }
-}
-
-/**
- * Iterate refresh records based on a filter.
- */
-async function iterRecordsForRefresh(
- tx: WalletDbReadOnlyTransaction<["refreshGroups"]>,
- filter: TransactionRecordFilter,
- f: (r: RefreshGroupRecord) => Promise<void>,
-): Promise<void> {
- let refreshGroups: RefreshGroupRecord[];
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- RefreshOperationStatus.Pending,
- RefreshOperationStatus.Suspended,
- );
- refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(keyRange);
- } else {
- refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll();
- }
-
- for (const r of refreshGroups) {
- await f(r);
- }
-}
-
-async function iterRecordsForWithdrawal(
- tx: WalletDbReadOnlyTransaction<["withdrawalGroups"]>,
- filter: TransactionRecordFilter,
- f: (r: WithdrawalGroupRecord) => Promise<void>,
-): Promise<void> {
- let withdrawalGroupRecords: WithdrawalGroupRecord[];
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- withdrawalGroupRecords =
- await tx.withdrawalGroups.indexes.byStatus.getAll(keyRange);
- } else {
- withdrawalGroupRecords =
- await tx.withdrawalGroups.indexes.byStatus.getAll();
- }
- for (const wgr of withdrawalGroupRecords) {
- await f(wgr);
- }
-}
-async function iterRecordsForDeposit(
- tx: WalletDbReadOnlyTransaction<["depositGroups"]>,
- filter: TransactionRecordFilter,
- f: (r: DepositGroupRecord) => Promise<void>,
-): Promise<void> {
- let dgs: DepositGroupRecord[];
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- dgs = await tx.depositGroups.indexes.byStatus.getAll(keyRange);
- } else {
- dgs = await tx.depositGroups.indexes.byStatus.getAll();
- }
-
- for (const dg of dgs) {
- await f(dg);
- }
-}
-
-async function iterRecordsForDenomLoss(
- tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>,
- filter: TransactionRecordFilter,
- f: (r: DenomLossEventRecord) => Promise<void>,
-): Promise<void> {
- let dgs: DenomLossEventRecord[];
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
- } else {
- dgs = await tx.denomLossEvents.indexes.byStatus.getAll();
- }
-
- for (const dg of dgs) {
- await f(dg);
- }
-}
-
-async function iterRecordsForRefund(
- tx: WalletDbReadOnlyTransaction<["refundGroups"]>,
- filter: TransactionRecordFilter,
- f: (r: RefundGroupRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.refundGroups.iter().forEachAsync(f);
- }
-}
-
-async function iterRecordsForPurchase(
- tx: WalletDbReadOnlyTransaction<["purchases"]>,
- filter: TransactionRecordFilter,
- f: (r: PurchaseRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.purchases.indexes.byStatus.iter().forEachAsync(f);
- }
-}
-
-async function iterRecordsForPeerPullCredit(
- tx: WalletDbReadOnlyTransaction<["peerPullCredit"]>,
- filter: TransactionRecordFilter,
- f: (r: PeerPullCreditRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.peerPullCredit.indexes.byStatus.iter().forEachAsync(f);
- }
-}
-
-async function iterRecordsForPeerPullDebit(
- tx: WalletDbReadOnlyTransaction<["peerPullDebit"]>,
- filter: TransactionRecordFilter,
- f: (r: PeerPullPaymentIncomingRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.peerPullDebit.indexes.byStatus.iter().forEachAsync(f);
- }
-}
-
-async function iterRecordsForPeerPushDebit(
- tx: WalletDbReadOnlyTransaction<["peerPushDebit"]>,
- filter: TransactionRecordFilter,
- f: (r: PeerPushDebitRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.peerPushDebit.indexes.byStatus.iter().forEachAsync(f);
- }
-}
-
-async function iterRecordsForPeerPushCredit(
- tx: WalletDbReadOnlyTransaction<["peerPushCredit"]>,
- filter: TransactionRecordFilter,
- f: (r: PeerPushPaymentIncomingRecord) => Promise<void>,
-): Promise<void> {
- if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.bound(
- OPERATION_STATUS_NONFINAL_FIRST,
- OPERATION_STATUS_NONFINAL_LAST,
- );
- await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f);
- } else {
- await tx.peerPushCredit.indexes.byStatus.iter().forEachAsync(f);
+ // As a heuristic, we emit balance-change notifications
+ // whenever the major state changes.
+ // This sometimes emits more notifications than we need,
+ // but makes it much more unlikely that we miss any.
+ if (transitionInfo.newTxState.major !== transitionInfo.oldTxState.major) {
+ wex.ws.notify({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
}
}
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index 8c1ac5fc2..8b4b24351 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -29,13 +29,6 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
export const WALLET_MERCHANT_PROTOCOL_VERSION = "5:0:1";
/**
- * Protocol version spoken with the bank (bank integration API).
- *
- * Uses libtool's current:revision:age versioning.
- */
-export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "1:0:0";
-
-/**
* Protocol version spoken with the bank (corebank API).
*
* Uses libtool's current:revision:age versioning.
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index aa88331ea..90f7ad913 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -29,6 +29,8 @@ import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult,
+ AcceptPeerPullPaymentResponse,
+ AcceptPeerPushPaymentResponse,
AcceptWithdrawalResponse,
AddExchangeRequest,
AddGlobalCurrencyAuditorRequest,
@@ -40,6 +42,8 @@ import {
BalancesResponse,
CanonicalizeBaseUrlRequest,
CanonicalizeBaseUrlResponse,
+ CheckDepositRequest,
+ CheckDepositResponse,
CheckPayTemplateReponse,
CheckPayTemplateRequest,
CheckPeerPullCreditRequest,
@@ -59,6 +63,7 @@ import {
DeleteExchangeRequest,
DeleteStoredBackupRequest,
DeleteTransactionRequest,
+ EmptyObject,
ExchangeDetailedResponse,
ExchangesListResponse,
ExchangesShortListResponse,
@@ -66,19 +71,26 @@ import {
ForceRefreshRequest,
ForgetKnownBankAccountsRequest,
GetActiveTasksResponse,
- GetAmountRequest,
GetBalanceDetailRequest,
+ GetBankingChoicesForPaytoRequest,
+ GetBankingChoicesForPaytoResponse,
GetContractTermsDetailsRequest,
GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
+ GetDepositWireTypesForCurrencyRequest,
+ GetDepositWireTypesForCurrencyResponse,
GetExchangeEntryByUrlRequest,
GetExchangeEntryByUrlResponse,
GetExchangeResourcesRequest,
GetExchangeResourcesResponse,
GetExchangeTosRequest,
GetExchangeTosResult,
- GetPlanForOperationRequest,
- GetPlanForOperationResponse,
+ GetMaxDepositAmountRequest,
+ GetMaxDepositAmountResponse,
+ GetMaxPeerPushDebitAmountRequest,
+ GetMaxPeerPushDebitAmountResponse,
+ GetQrCodesForPaytoRequest,
+ GetQrCodesForPaytoResponse,
GetWithdrawalDetailsForAmountRequest,
GetWithdrawalDetailsForUriRequest,
HintNetworkAvailabilityRequest,
@@ -90,6 +102,7 @@ import {
InitiatePeerPushDebitRequest,
InitiatePeerPushDebitResponse,
IntegrationTestArgs,
+ IntegrationTestV2Args,
KnownBankAccounts,
ListAssociatedRefreshesRequest,
ListAssociatedRefreshesResponse,
@@ -99,8 +112,6 @@ import {
ListKnownBankAccountsRequest,
PrepareBankIntegratedWithdrawalRequest,
PrepareBankIntegratedWithdrawalResponse,
- PrepareDepositRequest,
- PrepareDepositResponse,
PreparePayRequest,
PreparePayResult,
PreparePayTemplateRequest,
@@ -120,6 +131,7 @@ import {
SetWalletDeviceIdRequest,
SharePaymentRequest,
SharePaymentResult,
+ StartExchangeWalletKycRequest,
StartRefundQueryForUriResponse,
StartRefundQueryRequest,
StoredBackupList,
@@ -127,13 +139,13 @@ import {
TestPayResult,
TestingGetDenomStatsRequest,
TestingGetDenomStatsResponse,
- TestingListTasksForTransactionRequest,
- TestingListTasksForTransactionsResponse,
+ TestingGetReserveHistoryRequest,
TestingSetTimetravelRequest,
+ TestingWaitExchangeStateRequest,
TestingWaitTransactionRequest,
+ TestingWaitWalletKycRequest,
Transaction,
TransactionByIdRequest,
- TransactionWithdrawal,
TransactionsRequest,
TransactionsResponse,
TxIdResponse,
@@ -149,7 +161,6 @@ import {
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
WithdrawalDetailsForAmount,
- WithdrawalTransactionByURIRequest,
} from "@gnu-taler/taler-util";
import {
AddBackupProviderRequest,
@@ -177,7 +188,6 @@ export enum WalletApiOperation {
AddExchange = "addExchange",
GetTransactions = "getTransactions",
GetTransactionById = "getTransactionById",
- GetWithdrawalTransactionByUri = "getWithdrawalTransactionByUri",
TestingGetSampleTransactions = "testingGetSampleTransactions",
ListExchanges = "listExchanges",
GetExchangeEntryByUrl = "getExchangeEntryByUrl",
@@ -189,19 +199,15 @@ export enum WalletApiOperation {
AcceptManualWithdrawal = "acceptManualWithdrawal",
GetBalances = "getBalances",
GetBalanceDetail = "getBalanceDetail",
- GetPlanForOperation = "getPlanForOperation",
- ConvertDepositAmount = "ConvertDepositAmount",
- GetMaxDepositAmount = "GetMaxDepositAmount",
- ConvertPeerPushAmount = "ConvertPeerPushAmount",
- GetMaxPeerPushAmount = "GetMaxPeerPushAmount",
- ConvertWithdrawalAmount = "ConvertWithdrawalAmount",
+ ConvertDepositAmount = "convertDepositAmount",
+ GetMaxDepositAmount = "getMaxDepositAmount",
+ GetMaxPeerPushDebitAmount = "getMaxPeerPushDebitAmount",
GetUserAttentionRequests = "getUserAttentionRequests",
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
- GetPendingOperations = "getPendingOperations",
GetActiveTasks = "getActiveTasks",
SetExchangeTosAccepted = "setExchangeTosAccepted",
- SetExchangeTosForgotten = "SetExchangeTosForgotten",
+ SetExchangeTosForgotten = "setExchangeTosForgotten",
StartRefundQueryForUri = "startRefundQueryForUri",
StartRefundQuery = "startRefundQuery",
PrepareBankIntegratedWithdrawal = "prepareBankIntegratedWithdrawal",
@@ -227,7 +233,7 @@ export enum WalletApiOperation {
ExportBackupRecovery = "exportBackupRecovery",
ImportBackupRecovery = "importBackupRecovery",
GetBackupInfo = "getBackupInfo",
- PrepareDeposit = "prepareDeposit",
+ CheckDeposit = "checkDeposit",
GetVersion = "getVersion",
GenerateDepositGroupTxId = "generateDepositGroupTxId",
CreateDepositGroup = "createDepositGroup",
@@ -266,23 +272,31 @@ export enum WalletApiOperation {
Shutdown = "shutdown",
HintNetworkAvailability = "hintNetworkAvailability",
CanonicalizeBaseUrl = "canonicalizeBaseUrl",
+ GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency",
+ GetQrCodesForPayto = "getQrCodesForPayto",
+ GetBankingChoicesForPayto = "getBankingChoicesForPayto",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
TestingWaitTransactionState = "testingWaitTransactionState",
+ TestingWaitExchangeState = "testingWaitExchangeState",
TestingWaitTasksDone = "testingWaitTasksDone",
TestingSetTimetravel = "testingSetTimetravel",
- TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop",
- TestingListTaskForTransaction = "testingListTasksForTransaction",
TestingGetDenomStats = "testingGetDenomStats",
TestingPing = "testingPing",
TestingGetReserveHistory = "testingGetReserveHistory",
TestingResetAllRetries = "testingResetAllRetries",
+ StartExchangeWalletKyc = "startExchangeWalletKyc",
+ TestingWaitExchangeWalletKyc = "testingWaitWalletKyc",
+ HintApplicationResumed = "hintApplicationResumed",
+
+ /**
+ * @deprecated use checkDeposit instead
+ */
+ PrepareDeposit = "prepareDeposit",
}
// group: Initialization
-type EmptyObject = Record<string, never>;
-
/**
* Initialize wallet-core.
*
@@ -301,6 +315,17 @@ export type ShutdownOp = {
};
/**
+ * Give wallet-core a kick and restart all pending tasks.
+ * Useful when the host application gets suspended and resumed,
+ * and active network requests might have stalled.
+ */
+export type HintApplicationResumedOp = {
+ op: WalletApiOperation.HintApplicationResumed;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
* Change the configuration of wallet-core.
*
* Currently an alias for the initWallet request.
@@ -339,36 +364,22 @@ export type GetBalancesDetailOp = {
response: PaymentBalanceDetails;
};
-export type GetPlanForOperationOp = {
- op: WalletApiOperation.GetPlanForOperation;
- request: GetPlanForOperationRequest;
- response: GetPlanForOperationResponse;
-};
-
export type ConvertDepositAmountOp = {
op: WalletApiOperation.ConvertDepositAmount;
request: ConvertAmountRequest;
response: AmountResponse;
};
+
export type GetMaxDepositAmountOp = {
op: WalletApiOperation.GetMaxDepositAmount;
- request: GetAmountRequest;
- response: AmountResponse;
-};
-export type ConvertPeerPushAmountOp = {
- op: WalletApiOperation.ConvertPeerPushAmount;
- request: ConvertAmountRequest;
- response: AmountResponse;
+ request: GetMaxDepositAmountRequest;
+ response: GetMaxDepositAmountResponse;
};
-export type GetMaxPeerPushAmountOp = {
- op: WalletApiOperation.GetMaxPeerPushAmount;
- request: GetAmountRequest;
- response: AmountResponse;
-};
-export type ConvertWithdrawalAmountOp = {
- op: WalletApiOperation.ConvertWithdrawalAmount;
- request: ConvertAmountRequest;
- response: AmountResponse;
+
+export type GetMaxPeerPushDebitAmountOp = {
+ op: WalletApiOperation.GetMaxPeerPushDebitAmount;
+ request: GetMaxPeerPushDebitAmountRequest;
+ response: GetMaxPeerPushDebitAmountResponse;
};
// group: Managing Transactions
@@ -406,12 +417,6 @@ export type GetTransactionByIdOp = {
response: Transaction;
};
-export type GetWithdrawalTransactionByUriOp = {
- op: WalletApiOperation.GetWithdrawalTransactionByUri;
- request: WithdrawalTransactionByURIRequest;
- response: TransactionWithdrawal | undefined;
-};
-
export type RetryPendingNowOp = {
op: WalletApiOperation.RetryPendingNow;
request: EmptyObject;
@@ -511,7 +516,7 @@ export type PrepareBankIntegratedWithdrawalOp = {
export type ConfirmWithdrawalOp = {
op: WalletApiOperation.ConfirmWithdrawal;
request: ConfirmWithdrawalRequest;
- response: EmptyObject;
+ response: AcceptWithdrawalResponse;
};
/**
@@ -646,6 +651,18 @@ export type ListExchangesOp = {
response: ExchangesListResponse;
};
+export type StartExchangeWalletKycOp = {
+ op: WalletApiOperation.StartExchangeWalletKyc;
+ request: StartExchangeWalletKycRequest;
+ response: EmptyObject;
+};
+
+export type TestingWaitExchangeWalletKycOp = {
+ op: WalletApiOperation.TestingWaitExchangeWalletKyc;
+ request: TestingWaitWalletKycRequest;
+ response: EmptyObject;
+};
+
/**
* List exchanges that are available for withdrawing a particular
* scoped currency.
@@ -729,6 +746,16 @@ export type GetExchangeTosOp = {
};
/**
+ * Get wire types that can be used for a deposit operation
+ * with the provided currency.
+ */
+export type GetDepositWireTypesForCurrencyOp = {
+ op: WalletApiOperation.GetDepositWireTypesForCurrency;
+ request: GetDepositWireTypesForCurrencyRequest;
+ response: GetDepositWireTypesForCurrencyResponse;
+};
+
+/**
* Get the current terms of a service of an exchange.
*/
export type GetExchangeDetailedInfoOp = {
@@ -797,10 +824,19 @@ export type CreateDepositGroupOp = {
response: CreateDepositGroupResponse;
};
+export type CheckDepositOp = {
+ op: WalletApiOperation.CheckDeposit;
+ request: CheckDepositRequest;
+ response: CheckDepositResponse;
+};
+
+/**
+ * @deprecated use CheckDepositOp instead
+ */
export type PrepareDepositOp = {
op: WalletApiOperation.PrepareDeposit;
- request: PrepareDepositRequest;
- response: PrepareDepositResponse;
+ request: CheckDepositRequest;
+ response: CheckDepositResponse;
};
// group: Backups
@@ -933,7 +969,7 @@ export type PreparePeerPushCreditOp = {
export type ConfirmPeerPushCreditOp = {
op: WalletApiOperation.ConfirmPeerPushCredit;
request: ConfirmPeerPushCreditRequest;
- response: EmptyObject;
+ response: AcceptPeerPushPaymentResponse;
};
/**
@@ -969,7 +1005,7 @@ export type PreparePeerPullDebitOp = {
export type ConfirmPeerPullDebitOp = {
op: WalletApiOperation.ConfirmPeerPullDebit;
request: ConfirmPeerPullDebitRequest;
- response: EmptyObject;
+ response: AcceptPeerPullPaymentResponse;
};
// group: Data Validation
@@ -986,6 +1022,18 @@ export type CanonicalizeBaseUrlOp = {
response: CanonicalizeBaseUrlResponse;
};
+export type GetQrCodesForPaytoOp = {
+ op: WalletApiOperation.GetQrCodesForPayto;
+ request: GetQrCodesForPaytoRequest;
+ response: GetQrCodesForPaytoResponse;
+};
+
+export type GetBankingChoicesForPaytoOp = {
+ op: WalletApiOperation.GetBankingChoicesForPayto;
+ request: GetBankingChoicesForPaytoRequest;
+ response: GetBankingChoicesForPaytoResponse;
+};
+
// group: Database Management
/**
@@ -1051,7 +1099,7 @@ export type RunIntegrationTestOp = {
*/
export type RunIntegrationTestV2Op = {
op: WalletApiOperation.RunIntegrationTestV2;
- request: IntegrationTestArgs;
+ request: IntegrationTestV2Args;
response: EmptyObject;
};
@@ -1120,17 +1168,6 @@ export type GetUserAttentionsUnreadCount = {
response: UserAttentionsCountResponse;
};
-/**
- * Get wallet-internal pending tasks.
- *
- * @deprecated
- */
-export type GetPendingTasksOp = {
- op: WalletApiOperation.GetPendingOperations;
- request: EmptyObject;
- response: any;
-};
-
export type GetActiveTasksOp = {
op: WalletApiOperation.GetActiveTasks;
request: EmptyObject;
@@ -1156,15 +1193,6 @@ export type TestingSetTimetravelOp = {
};
/**
- * Add an offset to the wallet's internal time.
- */
-export type TestingListTasksForTransactionOp = {
- op: WalletApiOperation.TestingListTaskForTransaction;
- request: TestingListTasksForTransactionRequest;
- response: TestingListTasksForTransactionsResponse;
-};
-
-/**
* Wait until all transactions are in a final state.
*/
export type TestingWaitTransactionsFinalOp = {
@@ -1200,6 +1228,15 @@ export type TestingWaitTransactionStateOp = {
response: EmptyObject;
};
+/**
+ * Wait until an exchange entry is in a particular state.
+ */
+export type TestingWaitExchangeStateOp = {
+ op: WalletApiOperation.TestingWaitTransactionState;
+ request: TestingWaitExchangeStateRequest;
+ response: EmptyObject;
+};
+
export type TestingPingOp = {
op: WalletApiOperation.TestingPing;
request: EmptyObject;
@@ -1208,7 +1245,7 @@ export type TestingPingOp = {
export type TestingGetReserveHistoryOp = {
op: WalletApiOperation.TestingGetReserveHistory;
- request: EmptyObject;
+ request: TestingGetReserveHistoryRequest;
response: any;
};
@@ -1269,17 +1306,12 @@ export type WalletOperations = {
[WalletApiOperation.GetBalances]: GetBalancesOp;
[WalletApiOperation.ConvertDepositAmount]: ConvertDepositAmountOp;
[WalletApiOperation.GetMaxDepositAmount]: GetMaxDepositAmountOp;
- [WalletApiOperation.ConvertPeerPushAmount]: ConvertPeerPushAmountOp;
- [WalletApiOperation.GetMaxPeerPushAmount]: GetMaxPeerPushAmountOp;
- [WalletApiOperation.ConvertWithdrawalAmount]: ConvertWithdrawalAmountOp;
- [WalletApiOperation.GetPlanForOperation]: GetPlanForOperationOp;
+ [WalletApiOperation.GetMaxPeerPushDebitAmount]: GetMaxPeerPushDebitAmountOp;
[WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
[WalletApiOperation.GetTransactions]: GetTransactionsOp;
[WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp;
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
- [WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
- [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
[WalletApiOperation.GetActiveTasks]: GetActiveTasksOp;
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
@@ -1307,6 +1339,7 @@ export type WalletOperations = {
[WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp;
[WalletApiOperation.GetExchangeEntryByUrl]: GetExchangeEntryByUrlOp;
[WalletApiOperation.PrepareDeposit]: PrepareDepositOp;
+ [WalletApiOperation.CheckDeposit]: CheckDepositOp;
[WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
@@ -1340,6 +1373,7 @@ export type WalletOperations = {
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp;
[WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp;
[WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp;
+ [WalletApiOperation.TestingWaitExchangeState]: TestingWaitExchangeStateOp;
[WalletApiOperation.TestingWaitTasksDone]: TestingWaitTasksDoneOp;
[WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp;
[WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
@@ -1348,7 +1382,6 @@ export type WalletOperations = {
[WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
[WalletApiOperation.UpdateExchangeEntry]: UpdateExchangeEntryOp;
[WalletApiOperation.PrepareWithdrawExchange]: PrepareWithdrawExchangeOp;
- [WalletApiOperation.TestingInfiniteTransactionLoop]: any;
[WalletApiOperation.DeleteExchange]: DeleteExchangeOp;
[WalletApiOperation.GetExchangeResources]: GetExchangeResourcesOp;
[WalletApiOperation.ListGlobalCurrencyAuditors]: ListGlobalCurrencyAuditorsOp;
@@ -1358,7 +1391,6 @@ export type WalletOperations = {
[WalletApiOperation.AddGlobalCurrencyExchange]: AddGlobalCurrencyExchangeOp;
[WalletApiOperation.RemoveGlobalCurrencyExchange]: RemoveGlobalCurrencyExchangeOp;
[WalletApiOperation.ListAssociatedRefreshes]: ListAssociatedRefreshesOp;
- [WalletApiOperation.TestingListTaskForTransaction]: TestingListTasksForTransactionOp;
[WalletApiOperation.TestingGetDenomStats]: TestingGetDenomStatsOp;
[WalletApiOperation.TestingPing]: TestingPingOp;
[WalletApiOperation.Shutdown]: ShutdownOp;
@@ -1368,6 +1400,12 @@ export type WalletOperations = {
[WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
[WalletApiOperation.TestingResetAllRetries]: TestingResetAllRetriesOp;
[WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp;
+ [WalletApiOperation.GetDepositWireTypesForCurrency]: GetDepositWireTypesForCurrencyOp;
+ [WalletApiOperation.GetQrCodesForPayto]: GetQrCodesForPaytoOp;
+ [WalletApiOperation.GetBankingChoicesForPayto]: GetBankingChoicesForPaytoOp;
+ [WalletApiOperation.StartExchangeWalletKyc]: StartExchangeWalletKycOp;
+ [WalletApiOperation.TestingWaitExchangeWalletKyc]: TestingWaitExchangeWalletKycOp;
+ [WalletApiOperation.HintApplicationResumed]: HintApplicationResumedOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index f1d53b7d5..1e41fb689 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -24,27 +24,61 @@
*/
import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge";
import {
+ AbortTransactionRequest,
AbsoluteTime,
+ AcceptBankIntegratedWithdrawalRequest,
+ AcceptManualWithdrawalRequest,
+ AcceptManualWithdrawalResult,
+ AcceptWithdrawalResponse,
ActiveTask,
+ AddExchangeRequest,
+ AddGlobalCurrencyAuditorRequest,
+ AddGlobalCurrencyExchangeRequest,
+ AddKnownBankAccountsRequest,
AmountJson,
AmountString,
Amounts,
- AsyncCondition,
CancellationToken,
+ CanonicalizeBaseUrlRequest,
+ CanonicalizeBaseUrlResponse,
+ Codec,
CoinDumpJson,
CoinStatus,
+ ConfirmPayRequest,
+ ConfirmPayResult,
CoreApiResponse,
CreateStoredBackupResponse,
+ DeleteExchangeRequest,
DeleteStoredBackupRequest,
DenominationInfo,
Duration,
+ EmptyObject,
ExchangesShortListResponse,
+ FailTransactionRequest,
+ ForgetKnownBankAccountsRequest,
+ GetActiveTasksResponse,
+ GetBankingChoicesForPaytoRequest,
+ GetBankingChoicesForPaytoResponse,
+ GetContractTermsDetailsRequest,
+ GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
+ GetDepositWireTypesForCurrencyRequest,
+ GetDepositWireTypesForCurrencyResponse,
+ GetExchangeTosRequest,
+ GetExchangeTosResult,
+ GetQrCodesForPaytoRequest,
+ GetQrCodesForPaytoResponse,
+ HintNetworkAvailabilityRequest,
+ InitRequest,
InitResponse,
+ IntegrationTestArgs,
+ IntegrationTestV2Args,
KnownBankAccounts,
KnownBankAccountsInfo,
+ ListExchangesForScopedCurrencyRequest,
ListGlobalCurrencyAuditorsResponse,
ListGlobalCurrencyExchangesResponse,
+ ListKnownBankAccountsRequest,
Logger,
NotificationType,
ObservabilityContext,
@@ -55,21 +89,36 @@ import {
PrepareWithdrawExchangeRequest,
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
+ RemoveGlobalCurrencyAuditorRequest,
+ RemoveGlobalCurrencyExchangeRequest,
+ SharePaymentRequest,
+ SharePaymentResult,
+ StartRefundQueryRequest,
StoredBackupList,
+ SuspendTransactionRequest,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
TalerUriAction,
+ TestingGetDenomStatsRequest,
TestingGetDenomStatsResponse,
- TestingListTasksForTransactionsResponse,
- TestingWaitTransactionRequest,
+ TestingGetReserveHistoryRequest,
+ TestingSetTimetravelRequest,
TimerAPI,
TimerGroup,
+ TransactionIdStr,
TransactionType,
+ TransactionsResponse,
+ UpdateExchangeEntryRequest,
+ ValidateIbanRequest,
ValidateIbanResponse,
+ WalletContractData,
WalletCoreVersion,
WalletNotification,
WalletRunConfig,
+ WireTypeDetails,
+ WithdrawTestBalanceRequest,
canonicalizeBaseUrl,
checkDbInvariant,
codecForAbortTransaction,
@@ -84,6 +133,7 @@ import {
codecForAny,
codecForApplyDevExperiment,
codecForCanonicalizeBaseUrlRequest,
+ codecForCheckDepositRequest,
codecForCheckPayTemplateRequest,
codecForCheckPeerPullPaymentRequest,
codecForCheckPeerPushDebitRequest,
@@ -95,16 +145,21 @@ import {
codecForDeleteExchangeRequest,
codecForDeleteStoredBackupRequest,
codecForDeleteTransactionRequest,
+ codecForEmptyObject,
codecForFailTransactionRequest,
codecForForceRefreshRequest,
codecForForgetKnownBankAccounts,
- codecForGetAmountRequest,
codecForGetBalanceDetailRequest,
+ codecForGetBankingChoicesForPaytoRequest,
codecForGetContractTermsDetails,
codecForGetCurrencyInfoRequest,
+ codecForGetDepositWireTypesForCurrencyRequest,
codecForGetExchangeEntryByUrlRequest,
codecForGetExchangeResourcesRequest,
codecForGetExchangeTosRequest,
+ codecForGetMaxDepositAmountRequest,
+ codecForGetMaxPeerPushDebitAmountRequest,
+ codecForGetQrCodesForPaytoRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForHintNetworkAvailabilityRequest,
@@ -117,7 +172,6 @@ import {
codecForListExchangesForScopedCurrencyRequest,
codecForListKnownBankAccounts,
codecForPrepareBankIntegratedWithdrawalRequest,
- codecForPrepareDepositRequest,
codecForPreparePayRequest,
codecForPreparePayTemplateRequest,
codecForPreparePeerPullPaymentRequest,
@@ -132,13 +186,14 @@ import {
codecForSetCoinSuspendedRequest,
codecForSetWalletDeviceIdRequest,
codecForSharePaymentRequest,
+ codecForStartExchangeWalletKycRequest,
codecForStartRefundQueryRequest,
codecForSuspendTransaction,
codecForTestPayArgs,
codecForTestingGetDenomStatsRequest,
codecForTestingGetReserveHistoryRequest,
- codecForTestingListTasksForTransactionRequest,
codecForTestingSetTimetravelRequest,
+ codecForTestingWaitWalletKycRequest,
codecForTransactionByIdRequest,
codecForTransactionsRequest,
codecForUpdateExchangeEntryRequest,
@@ -147,13 +202,13 @@ import {
codecForValidateIbanRequest,
codecForWithdrawTestBalance,
getErrorDetailFromException,
+ getQrCodesForPayto,
j2s,
openPromise,
parsePaytoUri,
parseTalerUri,
performanceNow,
safeStringifyException,
- sampleWalletCoreTransactions,
setDangerousTimetravel,
validateIban,
} from "@gnu-taler/taler-util";
@@ -179,6 +234,10 @@ import {
setWalletDeviceId,
} from "./backup/index.js";
import { getBalanceDetail, getBalances } from "./balance.js";
+import {
+ getMaxDepositAmount,
+ getMaxPeerPushDebitAmount,
+} from "./coinSelection.js";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
CryptoDispatcher,
@@ -188,6 +247,7 @@ import {
CoinSourceType,
ConfigRecordKey,
DenominationRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletStoresV1,
clearDatabase,
@@ -214,16 +274,14 @@ import {
getExchangeDetailedInfo,
getExchangeResources,
getExchangeTos,
+ getExchangeWireDetailsInTx,
+ handleStartExchangeWalletKyc,
+ handleTestingWaitExchangeState,
+ handleTestingWaitExchangeWalletKyc,
listExchanges,
lookupExchangeByUri,
} from "./exchanges.js";
-import {
- convertDepositAmount,
- convertPeerPushAmount,
- convertWithdrawalAmount,
- getMaxDepositAmount,
- getMaxPeerPushAmount,
-} from "./instructedAmountConversion.js";
+import { convertDepositAmount } from "./instructedAmountConversion.js";
import {
ObservableDbAccess,
ObservableTaskScheduler,
@@ -240,7 +298,7 @@ import {
startRefundQueryForUri,
} from "./pay-merchant.js";
import {
- checkPeerPullPaymentInitiation,
+ checkPeerPullCredit,
initiatePeerPullPayment,
} from "./pay-peer-pull-credit.js";
import {
@@ -267,7 +325,6 @@ import {
TaskSchedulerImpl,
convertTaskToTransactionId,
getActiveTaskIds,
- listTaskForTransactionId,
} from "./shepherd.js";
import {
runIntegrationTest,
@@ -286,8 +343,9 @@ import {
failTransaction,
getTransactionById,
getTransactions,
- getWithdrawalTransactionByUri,
parseTransactionIdentifier,
+ rematerializeTransactions,
+ restartAll as restartAllRunningTasks,
resumeTransaction,
retryAll,
retryTransaction,
@@ -295,7 +353,6 @@ import {
} from "./transactions.js";
import {
WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_COREBANK_API_PROTOCOL_VERSION,
WALLET_CORE_API_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -304,10 +361,11 @@ import {
import {
WalletApiOperation,
WalletCoreApiClient,
+ WalletCoreRequestType,
WalletCoreResponseType,
} from "./wallet-api-types.js";
import {
- acceptWithdrawalFromUri,
+ acceptBankIntegratedWithdrawal,
confirmWithdrawal,
createManualWithdrawal,
getWithdrawalDetailsForAmount,
@@ -330,6 +388,7 @@ export interface WalletExecutionContext {
readonly http: HttpRequestLibrary;
readonly db: DbAccess<typeof WalletStoresV1>;
readonly oc: ObservabilityContext;
+ readonly cts: CancellationToken.Source | undefined;
readonly taskScheduler: TaskScheduler;
}
@@ -377,6 +436,41 @@ async function fillDefaults(wex: WalletExecutionContext): Promise<void> {
}
}
+/**
+ * Incremented each time we want to re-materialize transactions.
+ */
+const MATERIALIZED_TRANSACTIONS_VERSION = 1;
+
+async function migrateMaterializedTransactions(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ const ver = await tx.config.get("materializedTransactionsVersion");
+ if (ver) {
+ if (ver.key !== ConfigRecordKey.MaterializedTransactionsVersion) {
+ logger.error("invalid configuration (materializedTransactionsVersion)");
+ return;
+ }
+ if (ver.value == MATERIALIZED_TRANSACTIONS_VERSION) {
+ return;
+ }
+ if (ver.value > MATERIALIZED_TRANSACTIONS_VERSION) {
+ logger.error(
+ "database is newer than code (materializedTransactionsVersion)",
+ );
+ return;
+ }
+ }
+
+ await rematerializeTransactions(wex, tx);
+
+ await tx.config.put({
+ key: ConfigRecordKey.MaterializedTransactionsVersion,
+ value: MATERIALIZED_TRANSACTIONS_VERSION,
+ });
+ });
+}
+
export async function getDenomInfo(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<["denominations"]>,
@@ -401,11 +495,12 @@ export async function getDenomInfo(
* List bank accounts known to the wallet from
* previous withdrawals.
*/
-async function listKnownBankAccounts(
+async function handleListKnownBankAccounts(
wex: WalletExecutionContext,
- currency?: string,
+ req: ListKnownBankAccountsRequest,
): Promise<KnownBankAccounts> {
const accounts: KnownBankAccountsInfo[] = [];
+ const currency = req.currency;
await wex.db.runReadOnlyTx({ storeNames: ["bankAccounts"] }, async (tx) => {
const knownAccounts = await tx.bankAccounts.iter().toArray();
for (const r of knownAccounts) {
@@ -479,7 +574,10 @@ async function setCoinSuspended(
c.denomPubHash,
c.maxAge,
]);
- checkDbInvariant(!!coinAvailability, `no denom info for ${c.denomPubHash} age ${c.maxAge}`);
+ checkDbInvariant(
+ !!coinAvailability,
+ `no denom info for ${c.denomPubHash} age ${c.maxAge}`,
+ );
if (suspended) {
if (c.status !== CoinStatus.Fresh) {
return;
@@ -511,7 +609,7 @@ async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> {
const coinsJson: CoinDumpJson = { coins: [] };
logger.info("dumping coins");
await wex.db.runReadOnlyTx(
- { storeNames: ["coins", "denominations"] },
+ { storeNames: ["coins", "coinHistory", "denominations"] },
async (tx) => {
const coins = await tx.coins.iter().toArray();
for (const c of coins) {
@@ -542,22 +640,18 @@ async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> {
logger.warn("no denomination found for coin");
continue;
}
+ const historyRec = await tx.coinHistory.get(c.coinPub);
coinsJson.coins.push({
- coin_pub: c.coinPub,
- denom_pub: denomInfo.denomPub,
- denom_pub_hash: c.denomPubHash,
- denom_value: denom.value,
- exchange_base_url: c.exchangeBaseUrl,
- refresh_parent_coin_pub: refreshParentCoinPub,
- withdrawal_reserve_pub: withdrawalReservePub,
- coin_status: c.status,
+ coinPub: c.coinPub,
+ denomPub: denomInfo.denomPub,
+ denomPubHash: c.denomPubHash,
+ denomValue: denom.value,
+ exchangeBaseUrl: c.exchangeBaseUrl,
+ refreshParentCoinPub: refreshParentCoinPub,
+ withdrawalReservePub: withdrawalReservePub,
+ coinStatus: c.status,
ageCommitmentProof: c.ageCommitmentProof,
- spend_allocation: c.spendAllocation
- ? {
- amount: c.spendAllocation.amount,
- id: c.spendAllocation.id,
- }
- : undefined,
+ history: historyRec ? historyRec.history : [],
});
}
},
@@ -575,7 +669,12 @@ async function getClientFromWalletState(
const client: WalletCoreApiClient = {
async call(op, payload): Promise<any> {
id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100);
- const res = await handleCoreApiRequest(ws, op, String(id), payload);
+ const res = await dispatchWalletCoreApiRequest(
+ ws,
+ op,
+ String(id),
+ payload,
+ );
switch (res.type) {
case "error":
throw TalerError.fromUncheckedDetail(res.error);
@@ -652,6 +751,9 @@ async function recoverStoredBackup(
});
logger.info(`backup found, now importing`);
await importDb(wex.db.idbHandle(), bd);
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ await rematerializeTransactions(wex, tx);
+ });
logger.info(`import done`);
}
@@ -677,927 +779,1413 @@ async function handlePrepareWithdrawExchange(
};
}
-/**
- * Response returned from the pending operations API.
- *
- * @deprecated this is a placeholder for the response type of a deprecated wallet-core request.
- */
-export interface PendingOperationsResponse {
- /**
- * List of pending operations.
- */
- pendingOperations: any[];
+async function handleRetryPendingNow(
+ wex: WalletExecutionContext,
+): Promise<EmptyObject> {
+ logger.error("retryPendingNow currently not implemented");
+ return {};
}
-/**
- * Implementation of the "wallet-core" API.
- */
-async function dispatchRequestInternal(
+async function handleSharePayment(
wex: WalletExecutionContext,
- cts: CancellationToken.Source,
- operation: WalletApiOperation,
- payload: unknown,
-): Promise<WalletCoreResponseType<typeof operation>> {
- if (!wex.ws.initCalled && operation !== WalletApiOperation.InitWallet) {
- throw Error(
- `wallet must be initialized before running operation ${operation}`,
- );
+ req: SharePaymentRequest,
+): Promise<SharePaymentResult> {
+ return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
+}
+
+async function handleDeleteStoredBackup(
+ wex: WalletExecutionContext,
+ req: DeleteStoredBackupRequest,
+): Promise<EmptyObject> {
+ await deleteStoredBackup(wex, req);
+ return {};
+}
+
+async function handleRecoverStoredBackup(
+ wex: WalletExecutionContext,
+ req: RecoverStoredBackupRequest,
+): Promise<EmptyObject> {
+ await recoverStoredBackup(wex, req);
+ return {};
+}
+
+async function handleSetWalletRunConfig(
+ wex: WalletExecutionContext,
+ req: InitRequest,
+) {
+ if (logger.shouldLogTrace()) {
+ const initType = wex.ws.initCalled
+ ? "repeat initialization"
+ : "first initialization";
+ logger.trace(`init request (${initType}): ${j2s(req)}`);
}
- // FIXME: Can we make this more type-safe by using the request/response type
- // definitions we already have?
- switch (operation) {
- case WalletApiOperation.CreateStoredBackup:
- return createStoredBackup(wex);
- case WalletApiOperation.DeleteStoredBackup: {
- const req = codecForDeleteStoredBackupRequest().decode(payload);
- await deleteStoredBackup(wex, req);
- return {};
- }
- case WalletApiOperation.ListStoredBackups:
- return listStoredBackups(wex);
- case WalletApiOperation.RecoverStoredBackup: {
- const req = codecForRecoverStoredBackupRequest().decode(payload);
- await recoverStoredBackup(wex, req);
- return {};
- }
- case WalletApiOperation.SetWalletRunConfig:
- case WalletApiOperation.InitWallet: {
- const req = codecForInitRequest().decode(payload);
-
- if (logger.shouldLogTrace()) {
- const initType = wex.ws.initCalled
- ? "repeat initialization"
- : "first initialization";
- logger.trace(`init request (${initType}): ${j2s(req)}`);
- }
- // Write to the DB to make sure that we're failing early in
- // case the DB is not writeable.
- try {
- await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- tx.config.put({
- key: ConfigRecordKey.LastInitInfo,
- value: timestampProtocolToDb(TalerProtocolTimestamp.now()),
- });
- });
- } catch (e) {
- logger.error("error writing to database during initialization");
- throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
- innerError: getErrorDetailFromException(e),
- });
- }
- wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
+ // Write to the DB to make sure that we're failing early in
+ // case the DB is not writeable.
+ try {
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
+ tx.config.put({
+ key: ConfigRecordKey.LastInitInfo,
+ value: timestampProtocolToDb(TalerProtocolTimestamp.now()),
+ });
+ });
+ } catch (e) {
+ logger.error("error writing to database during initialization");
+ throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
+ innerError: getErrorDetailFromException(e),
+ });
+ }
+ wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
- if (wex.ws.config.testing.skipDefaults) {
- logger.trace("skipping defaults");
- } else {
- logger.trace("filling defaults");
- await fillDefaults(wex);
- }
- const resp: InitResponse = {
- versionInfo: getVersion(wex),
- };
+ if (wex.ws.config.testing.skipDefaults) {
+ logger.trace("skipping defaults");
+ } else {
+ logger.trace("filling defaults");
+ await fillDefaults(wex);
+ }
- if (req.config?.lazyTaskLoop) {
- logger.trace("lazily starting task loop");
- } else {
- await wex.taskScheduler.ensureRunning();
- }
+ await migrateMaterializedTransactions(wex);
+
+ const resp: InitResponse = {
+ versionInfo: await handleGetVersion(wex),
+ };
+
+ if (req.config?.lazyTaskLoop) {
+ logger.trace("lazily starting task loop");
+ } else {
+ await wex.taskScheduler.ensureRunning();
+ }
+
+ wex.ws.initCalled = true;
+ return resp;
+}
+
+async function handleWithdrawTestkudos(wex: WalletExecutionContext) {
+ await withdrawTestBalance(wex, {
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+ });
+ // FIXME: Is this correct?
+ return {};
+}
+
+async function handleWithdrawTestBalance(
+ wex: WalletExecutionContext,
+ req: WithdrawTestBalanceRequest,
+): Promise<EmptyObject> {
+ await withdrawTestBalance(wex, req);
+ return {};
+}
+
+async function handleRunIntegrationTest(
+ wex: WalletExecutionContext,
+ req: IntegrationTestArgs,
+): Promise<EmptyObject> {
+ await runIntegrationTest(wex, req);
+ return {};
+}
+
+async function handleRunIntegrationTestV2(
+ wex: WalletExecutionContext,
+ req: IntegrationTestV2Args,
+): Promise<EmptyObject> {
+ await runIntegrationTest2(wex, req);
+ return {};
+}
+
+async function handleValidateIban(
+ wex: WalletExecutionContext,
+ req: ValidateIbanRequest,
+): Promise<ValidateIbanResponse> {
+ const valRes = validateIban(req.iban);
+ const resp: ValidateIbanResponse = {
+ valid: valRes.type === "valid",
+ };
+ return resp;
+}
+
+async function handleAddExchange(
+ wex: WalletExecutionContext,
+ req: AddExchangeRequest,
+): Promise<EmptyObject> {
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {});
+ return {};
+}
+
+async function handleUpdateExchangeEntry(
+ wex: WalletExecutionContext,
+ req: UpdateExchangeEntryRequest,
+): Promise<EmptyObject> {
+ await fetchFreshExchange(wex, req.exchangeBaseUrl, {
+ forceUpdate: !!req.force,
+ });
+ return {};
+}
- wex.ws.initCalled = true;
- return resp;
+async function handleTestingGetDenomStats(
+ wex: WalletExecutionContext,
+ req: TestingGetDenomStatsRequest,
+): Promise<TestingGetDenomStatsResponse> {
+ const denomStats: TestingGetDenomStatsResponse = {
+ numKnown: 0,
+ numLost: 0,
+ numOffered: 0,
+ };
+ await wex.db.runReadOnlyTx({ storeNames: ["denominations"] }, async (tx) => {
+ const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ req.exchangeBaseUrl,
+ );
+ for (const d of denoms) {
+ denomStats.numKnown++;
+ if (d.isOffered) {
+ denomStats.numOffered++;
+ }
+ if (d.isLost) {
+ denomStats.numLost++;
+ }
}
- case WalletApiOperation.WithdrawTestkudos: {
- await withdrawTestBalance(wex, {
- amount: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
+ });
+ return denomStats;
+}
+
+async function handleListExchangesForScopedCurrency(
+ wex: WalletExecutionContext,
+ req: ListExchangesForScopedCurrencyRequest,
+): Promise<ExchangesShortListResponse> {
+ const exchangesResp = await listExchanges(wex);
+ const result: ExchangesShortListResponse = {
+ exchanges: [],
+ };
+ // Right now we only filter on the currency, as wallet-core doesn't
+ // fully support scoped currencies yet.
+ for (const exch of exchangesResp.exchanges) {
+ if (exch.currency === req.scope.currency) {
+ result.exchanges.push({
+ exchangeBaseUrl: exch.exchangeBaseUrl,
});
- return {
- versionInfo: getVersion(wex),
- };
}
- case WalletApiOperation.WithdrawTestBalance: {
- const req = codecForWithdrawTestBalance().decode(payload);
- await withdrawTestBalance(wex, req);
- return {};
+ }
+ return result;
+}
+
+async function handleAddKnownBankAccount(
+ wex: WalletExecutionContext,
+ req: AddKnownBankAccountsRequest,
+): Promise<EmptyObject> {
+ await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
+ return {};
+}
+
+async function handleForgetKnownBankAccounts(
+ wex: WalletExecutionContext,
+ req: ForgetKnownBankAccountsRequest,
+): Promise<EmptyObject> {
+ await forgetKnownBankAccounts(wex, req.payto);
+ return {};
+}
+
+// FIXME: Doesn't have proper type!
+async function handleTestingGetReserveHistory(
+ wex: WalletExecutionContext,
+ req: TestingGetReserveHistoryRequest,
+): Promise<any> {
+ const reserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return tx.reserves.indexes.byReservePub.get(req.reservePub);
+ },
+ );
+ if (!reserve) {
+ throw Error("no reserve pub found");
+ }
+ const sigResp = await wex.cryptoApi.signReserveHistoryReq({
+ reservePriv: reserve.reservePriv,
+ startOffset: 0,
+ });
+ const exchangeBaseUrl = req.exchangeBaseUrl;
+ const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl);
+ const resp = await wex.http.fetch(url.href, {
+ headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig },
+ });
+ const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ return historyJson;
+}
+
+async function handleAcceptManualWithdrawal(
+ wex: WalletExecutionContext,
+ req: AcceptManualWithdrawalRequest,
+): Promise<AcceptManualWithdrawalResult> {
+ const res = await createManualWithdrawal(wex, {
+ amount: Amounts.parseOrThrow(req.amount),
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ restrictAge: req.restrictAge,
+ forceReservePriv: req.forceReservePriv,
+ });
+ return res;
+}
+
+async function handleGetExchangeTos(
+ wex: WalletExecutionContext,
+ req: GetExchangeTosRequest,
+): Promise<GetExchangeTosResult> {
+ return getExchangeTos(
+ wex,
+ req.exchangeBaseUrl,
+ req.acceptedFormat,
+ req.acceptLanguage,
+ );
+}
+
+async function handleGetContractTermsDetails(
+ wex: WalletExecutionContext,
+ req: GetContractTermsDetailsRequest,
+): Promise<WalletContractData> {
+ if (req.proposalId) {
+ // FIXME: deprecated path
+ return getContractTermsDetails(wex, req.proposalId);
+ }
+ if (req.transactionId) {
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+ if (parsedTx?.tag === TransactionType.Payment) {
+ return getContractTermsDetails(wex, parsedTx.proposalId);
}
- case WalletApiOperation.TestingListTaskForTransaction: {
- const req =
- codecForTestingListTasksForTransactionRequest().decode(payload);
+ throw Error("transactionId is not a payment transaction");
+ }
+ throw Error("transactionId missing");
+}
+
+async function handleGetQrCodesForPayto(
+ wex: WalletExecutionContext,
+ req: GetQrCodesForPaytoRequest,
+): Promise<GetQrCodesForPaytoResponse> {
+ return {
+ codes: getQrCodesForPayto(req.paytoUri),
+ };
+}
+
+async function handleGetBankingChoicesForPayto(
+ wex: WalletExecutionContext,
+ req: GetBankingChoicesForPaytoRequest,
+): Promise<GetBankingChoicesForPaytoResponse> {
+ const parsedPayto = parsePaytoUri(req.paytoUri);
+ if (!parsedPayto) {
+ throw Error("invalid payto URI");
+ }
+ const amount = parsedPayto.params["amount"];
+ if (!amount) {
+ logger.warn("payto URI has no amount");
+ return {
+ choices: [],
+ };
+ }
+ const currency = Amounts.currencyOf(amount);
+ switch (currency) {
+ case "KUDOS":
return {
- taskIdList: listTaskForTransactionId(req.transactionId),
- } satisfies TestingListTasksForTransactionsResponse;
- }
- case WalletApiOperation.RunIntegrationTest: {
- const req = codecForIntegrationTestArgs().decode(payload);
- await runIntegrationTest(wex, req);
- return {};
- }
- case WalletApiOperation.RunIntegrationTestV2: {
- const req = codecForIntegrationTestV2Args().decode(payload);
- await runIntegrationTest2(wex, req);
- return {};
- }
- case WalletApiOperation.ValidateIban: {
- const req = codecForValidateIbanRequest().decode(payload);
- const valRes = validateIban(req.iban);
- const resp: ValidateIbanResponse = {
- valid: valRes.type === "valid",
+ choices: [
+ {
+ label: "Demobank Website",
+ type: "link",
+ uri: `https://bank.demo.taler.net/webui/#/transfer/${encodeURIComponent(
+ req.paytoUri,
+ )}`,
+ },
+ {
+ label: "Demobank App",
+ type: "link",
+ uri: `https://bank.demo.taler.net/app/transfer/${encodeURIComponent(
+ req.paytoUri,
+ )}`,
+ },
+ ],
};
- return resp;
- }
- case WalletApiOperation.TestPay: {
- const req = codecForTestPayArgs().decode(payload);
- return await testPay(wex, req);
- }
- case WalletApiOperation.GetTransactions: {
- const req = codecForTransactionsRequest().decode(payload);
- return await getTransactions(wex, req);
- }
- case WalletApiOperation.GetTransactionById: {
- const req = codecForTransactionByIdRequest().decode(payload);
- return await getTransactionById(wex, req);
- }
- case WalletApiOperation.GetWithdrawalTransactionByUri: {
- const req = codecForGetWithdrawalDetailsForUri().decode(payload);
- return await getWithdrawalTransactionByUri(wex, req);
- }
- case WalletApiOperation.AddExchange: {
- const req = codecForAddExchangeRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {});
- return {};
- }
- case WalletApiOperation.TestingPing: {
- return {};
- }
- case WalletApiOperation.UpdateExchangeEntry: {
- const req = codecForUpdateExchangeEntryRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {
- forceUpdate: !!req.force,
- });
- return {};
- }
- case WalletApiOperation.TestingGetDenomStats: {
- const req = codecForTestingGetDenomStatsRequest().decode(payload);
- const denomStats: TestingGetDenomStatsResponse = {
- numKnown: 0,
- numLost: 0,
- numOffered: 0,
+ break;
+ default:
+ return {
+ choices: [],
};
- await wex.db.runReadOnlyTx(
- { storeNames: ["denominations"] },
+ }
+}
+
+async function handleConfirmPay(
+ wex: WalletExecutionContext,
+ req: ConfirmPayRequest,
+): Promise<ConfirmPayResult> {
+ let transactionId;
+ if (req.proposalId) {
+ // legacy client support
+ transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: req.proposalId,
+ });
+ } else if (req.transactionId) {
+ transactionId = req.transactionId;
+ } else {
+ throw Error("transactionId or (deprecated) proposalId required");
+ }
+ return await confirmPay(wex, transactionId, req.sessionId);
+}
+
+async function handleAbortTransaction(
+ wex: WalletExecutionContext,
+ req: AbortTransactionRequest,
+): Promise<EmptyObject> {
+ await abortTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleSuspendTransaction(
+ wex: WalletExecutionContext,
+ req: SuspendTransactionRequest,
+): Promise<EmptyObject> {
+ await suspendTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleGetActiveTasks(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<GetActiveTasksResponse> {
+ const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
+
+ const tasksInfo = await Promise.all(
+ allTasksId.map(async (id) => {
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["operationRetries"] },
async (tx) => {
- const denoms =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- req.exchangeBaseUrl,
- );
- for (const d of denoms) {
- denomStats.numKnown++;
- if (d.isOffered) {
- denomStats.numOffered++;
+ return tx.operationRetries.get(id);
+ },
+ );
+ }),
+ );
+
+ const tasks = allTasksId.map((taskId, i): ActiveTask => {
+ const transaction = convertTaskToTransactionId(taskId);
+ const d = tasksInfo[i];
+
+ const firstTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.firstTry);
+ const nextTry = !d
+ ? undefined
+ : timestampAbsoluteFromDb(d.retryInfo.nextRetry);
+ const counter = d?.retryInfo.retryCounter;
+ const lastError = d?.lastError;
+
+ return {
+ taskId: taskId,
+ retryCounter: counter,
+ firstTry,
+ nextTry,
+ lastError,
+ transaction,
+ };
+ });
+ return { tasks };
+}
+
+async function handleFailTransaction(
+ wex: WalletExecutionContext,
+ req: FailTransactionRequest,
+): Promise<EmptyObject> {
+ await failTransaction(wex, req.transactionId);
+ return {};
+}
+
+async function handleTestingGetSampleTransactions(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<TransactionsResponse> {
+ // FIXME!
+ return { transactions: [] };
+ // These are out of date!
+ //return { transactions: sampleWalletCoreTransactions };
+}
+
+async function handleStartRefundQuery(
+ wex: WalletExecutionContext,
+ req: StartRefundQueryRequest,
+): Promise<EmptyObject> {
+ const txIdParsed = parseTransactionIdentifier(req.transactionId);
+ if (!txIdParsed) {
+ throw Error("invalid transaction ID");
+ }
+ if (txIdParsed.tag !== TransactionType.Payment) {
+ throw Error("expected payment transaction ID");
+ }
+ await startQueryRefund(wex, txIdParsed.proposalId);
+ return {};
+}
+
+async function handleHintNetworkAvailability(
+ wex: WalletExecutionContext,
+ req: HintNetworkAvailabilityRequest,
+): Promise<EmptyObject> {
+ // If network was already available, don't do anything
+ if (wex.ws.networkAvailable === req.isNetworkAvailable) {
+ return {};
+ }
+ wex.ws.networkAvailable = req.isNetworkAvailable;
+ // When network becomes available, restart tasks as they're blocked
+ // waiting for the network.
+ // When network goes down, restart tasks so they notice the network
+ // is down and wait.
+ await restartAllRunningTasks(wex);
+ return {};
+}
+
+async function handleGetDepositWireTypesForCurrency(
+ wex: WalletExecutionContext,
+ req: GetDepositWireTypesForCurrencyRequest,
+): Promise<GetDepositWireTypesForCurrencyResponse> {
+ const wtSet: Set<string> = new Set();
+ const wireTypeDetails: WireTypeDetails[] = [];
+ const talerBankHostnames: string[] = [];
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const exchanges = await tx.exchanges.getAll();
+ for (const exchange of exchanges) {
+ const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl);
+ if (!det) {
+ continue;
+ }
+ if (det.currency !== req.currency) {
+ continue;
+ }
+ for (const acc of det.wireInfo.accounts) {
+ let usable = true;
+ for (const dr of acc.debit_restrictions) {
+ if (dr.type === "deny") {
+ usable = false;
+ break;
}
- if (d.isLost) {
- denomStats.numLost++;
+ }
+ if (!usable) {
+ continue;
+ }
+ const parsedPayto = parsePaytoUri(acc.payto_uri);
+ if (!parsedPayto) {
+ continue;
+ }
+ if (
+ parsedPayto.isKnown &&
+ parsedPayto.targetType === "x-taler-bank"
+ ) {
+ if (!talerBankHostnames.includes(parsedPayto.host)) {
+ talerBankHostnames.push(parsedPayto.host);
}
}
- },
- );
- return denomStats;
- }
- case WalletApiOperation.ListExchanges: {
- return await listExchanges(wex);
- }
- case WalletApiOperation.GetExchangeEntryByUrl: {
- const req = codecForGetExchangeEntryByUrlRequest().decode(payload);
- return lookupExchangeByUri(wex, req);
- }
- case WalletApiOperation.ListExchangesForScopedCurrency: {
- const req =
- codecForListExchangesForScopedCurrencyRequest().decode(payload);
- const exchangesResp = await listExchanges(wex);
- const result: ExchangesShortListResponse = {
- exchanges: [],
- };
- // Right now we only filter on the currency, as wallet-core doesn't
- // fully support scoped currencies yet.
- for (const exch of exchangesResp.exchanges) {
- if (exch.currency === req.scope.currency) {
- result.exchanges.push({
- exchangeBaseUrl: exch.exchangeBaseUrl,
- });
+ if (!wtSet.has(parsedPayto.targetType)) {
+ wtSet.add(parsedPayto.targetType);
+ wireTypeDetails.push({
+ paymentTargetType: parsedPayto.targetType,
+ // Will possibly extended later by other exchanges
+ // with the same wire type.
+ talerBankHostnames,
+ });
+ }
}
}
- return result;
- }
- case WalletApiOperation.GetExchangeDetailedInfo: {
- const req = codecForAddExchangeRequest().decode(payload);
- return await getExchangeDetailedInfo(wex, req.exchangeBaseUrl);
- }
- case WalletApiOperation.ListKnownBankAccounts: {
- const req = codecForListKnownBankAccounts().decode(payload);
- return await listKnownBankAccounts(wex, req.currency);
- }
- case WalletApiOperation.AddKnownBankAccounts: {
- const req = codecForAddKnownBankAccounts().decode(payload);
- await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
- return {};
- }
- case WalletApiOperation.ForgetKnownBankAccounts: {
- const req = codecForForgetKnownBankAccounts().decode(payload);
- await forgetKnownBankAccounts(wex, req.payto);
- return {};
- }
- case WalletApiOperation.GetWithdrawalDetailsForUri: {
- const req = codecForGetWithdrawalDetailsForUri().decode(payload);
- return await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri);
- }
- case WalletApiOperation.TestingGetReserveHistory: {
- const req = codecForTestingGetReserveHistoryRequest().decode(payload);
- const reserve = await wex.db.runReadOnlyTx(
- { storeNames: ["reserves"] },
- async (tx) => {
- return tx.reserves.indexes.byReservePub.get(req.reservePub);
- },
- );
- if (!reserve) {
- throw Error("no reserve pub found");
+ },
+ );
+ return {
+ wireTypes: [...wtSet],
+ wireTypeDetails,
+ };
+}
+
+async function handleListGlobalCurrencyExchanges(
+ wex: WalletExecutionContext,
+ _req: EmptyObject,
+): Promise<ListGlobalCurrencyExchangesResponse> {
+ const resp: ListGlobalCurrencyExchangesResponse = {
+ exchanges: [],
+ };
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const gceList = await tx.globalCurrencyExchanges.iter().toArray();
+ for (const gce of gceList) {
+ resp.exchanges.push({
+ currency: gce.currency,
+ exchangeBaseUrl: gce.exchangeBaseUrl,
+ exchangeMasterPub: gce.exchangeMasterPub,
+ });
}
- const sigResp = await wex.cryptoApi.signReserveHistoryReq({
- reservePriv: reserve.reservePriv,
- startOffset: 0,
- });
- const exchangeBaseUrl = req.exchangeBaseUrl;
- const url = new URL(
- `reserves/${req.reservePub}/history`,
- exchangeBaseUrl,
- );
- const resp = await wex.http.fetch(url.href, {
- headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig },
- });
- const historyJson = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAny(),
- );
- return historyJson;
- }
- case WalletApiOperation.AcceptManualWithdrawal: {
- const req = codecForAcceptManualWithdrawalRequest().decode(payload);
- const res = await createManualWithdrawal(wex, {
- amount: Amounts.parseOrThrow(req.amount),
+ },
+ );
+ return resp;
+}
+
+async function handleListGlobalCurrencyAuditors(
+ wex: WalletExecutionContext,
+ _req: EmptyObject,
+): Promise<ListGlobalCurrencyAuditorsResponse> {
+ const resp: ListGlobalCurrencyAuditorsResponse = {
+ auditors: [],
+ };
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const gcaList = await tx.globalCurrencyAuditors.iter().toArray();
+ for (const gca of gcaList) {
+ resp.auditors.push({
+ currency: gca.currency,
+ auditorBaseUrl: gca.auditorBaseUrl,
+ auditorPub: gca.auditorPub,
+ });
+ }
+ },
+ );
+ return resp;
+}
+
+async function handleAddGlobalCurrencyExchange(
+ wex: WalletExecutionContext,
+ req: AddGlobalCurrencyExchangeRequest,
+): Promise<EmptyObject> {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const key = [req.currency, req.exchangeBaseUrl, req.exchangeMasterPub];
+ const existingRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (existingRec) {
+ return;
+ }
+ wex.ws.exchangeCache.clear();
+ await tx.globalCurrencyExchanges.add({
+ currency: req.currency,
exchangeBaseUrl: req.exchangeBaseUrl,
- restrictAge: req.restrictAge,
- forceReservePriv: req.forceReservePriv,
+ exchangeMasterPub: req.exchangeMasterPub,
});
- return res;
- }
- case WalletApiOperation.GetWithdrawalDetailsForAmount: {
- const req =
- codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
- const resp = await getWithdrawalDetailsForAmount(wex, cts, req);
- return resp;
- }
- case WalletApiOperation.GetBalances: {
- return await getBalances(wex);
- }
- case WalletApiOperation.GetBalanceDetail: {
- const req = codecForGetBalanceDetailRequest().decode(payload);
- return await getBalanceDetail(wex, req);
- }
- case WalletApiOperation.GetUserAttentionRequests: {
- const req = codecForUserAttentionsRequest().decode(payload);
- return await getUserAttentions(wex, req);
- }
- case WalletApiOperation.MarkAttentionRequestAsRead: {
- const req = codecForUserAttentionByIdRequest().decode(payload);
- return await markAttentionRequestAsRead(wex, req);
- }
- case WalletApiOperation.GetUserAttentionUnreadCount: {
- const req = codecForUserAttentionsRequest().decode(payload);
- return await getUserAttentionsUnreadCount(wex, req);
- }
- case WalletApiOperation.GetPendingOperations: {
- // FIXME: Eventually remove the handler after deprecation period.
- return {
- pendingOperations: [],
- } satisfies PendingOperationsResponse;
- }
- case WalletApiOperation.SetExchangeTosAccepted: {
- const req = codecForAcceptExchangeTosRequest().decode(payload);
- await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
- return {};
- }
- case WalletApiOperation.SetExchangeTosForgotten: {
- const req = codecForAcceptExchangeTosRequest().decode(payload);
- await forgetExchangeTermsOfService(wex, req.exchangeBaseUrl);
- return {};
- }
- case WalletApiOperation.AcceptBankIntegratedWithdrawal: {
- const req =
- codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
- return await acceptWithdrawalFromUri(wex, {
- selectedExchange: req.exchangeBaseUrl,
- talerWithdrawUri: req.talerWithdrawUri,
- forcedDenomSel: req.forcedDenomSel,
- restrictAge: req.restrictAge,
- amount: req.amount,
- });
- }
- case WalletApiOperation.ConfirmWithdrawal: {
- const req = codecForConfirmWithdrawalRequestRequest().decode(payload);
- return confirmWithdrawal(wex, req);
- }
- case WalletApiOperation.PrepareBankIntegratedWithdrawal: {
- const req =
- codecForPrepareBankIntegratedWithdrawalRequest().decode(payload);
- return prepareBankIntegratedWithdrawal(wex, req);
- }
- case WalletApiOperation.GetExchangeTos: {
- const req = codecForGetExchangeTosRequest().decode(payload);
- return getExchangeTos(
- wex,
- req.exchangeBaseUrl,
- req.acceptedFormat,
- req.acceptLanguage,
- );
- }
- case WalletApiOperation.GetContractTermsDetails: {
- const req = codecForGetContractTermsDetails().decode(payload);
- if (req.proposalId) {
- // FIXME: deprecated path
- return getContractTermsDetails(wex, req.proposalId);
+ },
+ );
+ return {};
+}
+
+async function handleRemoveGlobalCurrencyAuditor(
+ wex: WalletExecutionContext,
+ req: RemoveGlobalCurrencyAuditorRequest,
+): Promise<EmptyObject> {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
+ const existingRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(key);
+ if (!existingRec) {
+ return;
}
- if (req.transactionId) {
- const parsedTx = parseTransactionIdentifier(req.transactionId);
- if (parsedTx?.tag === TransactionType.Payment) {
- return getContractTermsDetails(wex, parsedTx.proposalId);
- }
- throw Error("transactionId is not a payment transaction");
+ checkDbInvariant(!!existingRec.id, `no global currency for ${j2s(key)}`);
+ await tx.globalCurrencyAuditors.delete(existingRec.id);
+ wex.ws.exchangeCache.clear();
+ },
+ );
+ return {};
+}
+
+async function handleRemoveGlobalCurrencyExchange(
+ wex: WalletExecutionContext,
+ req: RemoveGlobalCurrencyExchangeRequest,
+): Promise<EmptyObject> {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const key = [req.currency, req.exchangeBaseUrl, req.exchangeMasterPub];
+ const existingRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (!existingRec) {
+ return;
}
- throw Error("transactionId missing");
- }
- case WalletApiOperation.RetryPendingNow: {
- logger.error("retryPendingNow currently not implemented");
- return {};
- }
- case WalletApiOperation.SharePayment: {
- const req = codecForSharePaymentRequest().decode(payload);
- return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
- }
- case WalletApiOperation.PrepareWithdrawExchange: {
- const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
- return handlePrepareWithdrawExchange(wex, req);
- }
- case WalletApiOperation.CheckPayForTemplate: {
- const req = codecForCheckPayTemplateRequest().decode(payload);
- return await checkPayForTemplate(wex, req);
- }
- case WalletApiOperation.PreparePayForUri: {
- const req = codecForPreparePayRequest().decode(payload);
- return await preparePayForUri(wex, req.talerPayUri);
- }
- case WalletApiOperation.PreparePayForTemplate: {
- const req = codecForPreparePayTemplateRequest().decode(payload);
- return preparePayForTemplate(wex, req);
- }
- case WalletApiOperation.ConfirmPay: {
- const req = codecForConfirmPayRequest().decode(payload);
- let transactionId;
- if (req.proposalId) {
- // legacy client support
- transactionId = constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: req.proposalId,
- });
- } else if (req.transactionId) {
- transactionId = req.transactionId;
- } else {
- throw Error("transactionId or (deprecated) proposalId required");
+ wex.ws.exchangeCache.clear();
+ checkDbInvariant(!!existingRec.id, `no global exchange for ${j2s(key)}`);
+ await tx.globalCurrencyExchanges.delete(existingRec.id);
+ },
+ );
+ return {};
+}
+
+async function handleAddGlobalCurrencyAuditor(
+ wex: WalletExecutionContext,
+ req: AddGlobalCurrencyAuditorRequest,
+): Promise<EmptyObject> {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
+ const existingRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(key);
+ if (existingRec) {
+ return;
}
- return await confirmPay(wex, transactionId, req.sessionId);
- }
- case WalletApiOperation.AbortTransaction: {
- const req = codecForAbortTransaction().decode(payload);
- await abortTransaction(wex, req.transactionId);
+ await tx.globalCurrencyAuditors.add({
+ currency: req.currency,
+ auditorBaseUrl: req.auditorBaseUrl,
+ auditorPub: req.auditorPub,
+ });
+ wex.ws.exchangeCache.clear();
+ },
+ );
+ return {};
+}
+
+async function handleShutdown(
+ wex: WalletExecutionContext,
+ _req: EmptyObject,
+): Promise<EmptyObject> {
+ wex.ws.stop();
+ return {};
+}
+
+async function handleTestingSetTimetravel(
+ wex: WalletExecutionContext,
+ req: TestingSetTimetravelRequest,
+): Promise<EmptyObject> {
+ setDangerousTimetravel(req.offsetMs);
+ await wex.taskScheduler.reload();
+ return {};
+}
+
+async function handleCanonicalizeBaseUrl(
+ _wex: WalletExecutionContext,
+ req: CanonicalizeBaseUrlRequest,
+): Promise<CanonicalizeBaseUrlResponse> {
+ return {
+ url: canonicalizeBaseUrl(req.url),
+ };
+}
+
+async function handleDeleteExchange(
+ wex: WalletExecutionContext,
+ req: DeleteExchangeRequest,
+): Promise<EmptyObject> {
+ await deleteExchange(wex, req);
+ return {};
+}
+
+async function handleCreateStoredBackup(
+ wex: WalletExecutionContext,
+ _req: EmptyObject,
+): Promise<CreateStoredBackupResponse> {
+ return await createStoredBackup(wex);
+}
+
+async function handleAcceptBankIntegratedWithdrawal(
+ wex: WalletExecutionContext,
+ req: AcceptBankIntegratedWithdrawalRequest,
+): Promise<AcceptWithdrawalResponse> {
+ return await acceptBankIntegratedWithdrawal(wex, {
+ selectedExchange: req.exchangeBaseUrl,
+ talerWithdrawUri: req.talerWithdrawUri,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ amount: req.amount,
+ });
+}
+
+async function handleGetCurrencySpecification(
+ wex: WalletExecutionContext,
+ req: GetCurrencySpecificationRequest,
+): Promise<GetCurrencySpecificationResponse> {
+ const spec = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["currencyInfo"],
+ },
+ async (tx) => {
+ return WalletDbHelpers.getCurrencyInfo(tx, req.scope);
+ },
+ );
+ if (spec) {
+ return {
+ currencySpecification: spec.currencySpec,
+ };
+ }
+ // Hard-coded mock for KUDOS and TESTKUDOS
+ if (req.scope.currency === "KUDOS") {
+ const kudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Kudos (Taler Demonstrator)",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": "ク",
+ },
+ },
+ };
+ return kudosResp;
+ } else if (req.scope.currency === "TESTKUDOS") {
+ const testkudosResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: "Test (Taler Unstable Demonstrator)",
+ num_fractional_input_digits: 0,
+ num_fractional_normal_digits: 0,
+ num_fractional_trailing_zero_digits: 0,
+ alt_unit_names: {
+ "0": "テ",
+ },
+ },
+ };
+ return testkudosResp;
+ }
+ const defaultResp: GetCurrencySpecificationResponse = {
+ currencySpecification: {
+ name: req.scope.currency,
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ alt_unit_names: {
+ "0": req.scope.currency,
+ },
+ },
+ };
+ return defaultResp;
+}
+
+export async function handleHintApplicationResumed(
+ wex: WalletExecutionContext,
+ req: EmptyObject,
+): Promise<EmptyObject> {
+ logger.info("handling hintApplicationResumed");
+ await restartAllRunningTasks(wex);
+ return {};
+}
+
+async function handleGetVersion(
+ wex: WalletExecutionContext,
+): Promise<WalletCoreVersion> {
+ const result: WalletCoreVersion = {
+ implementationSemver: walletCoreBuildInfo.implementationSemver,
+ implementationGitHash: walletCoreBuildInfo.implementationGitHash,
+ hash: undefined,
+ version: WALLET_CORE_API_PROTOCOL_VERSION,
+ exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
+ merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
+ bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
+ bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
+ corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
+ bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
+ devMode: wex.ws.config.testing.devModeActive,
+ };
+ return result;
+}
+
+interface HandlerWithValidator<Tag extends WalletApiOperation> {
+ codec: Codec<WalletCoreRequestType<Tag>>;
+ handler: (
+ wex: WalletExecutionContext,
+ req: WalletCoreRequestType<Tag>,
+ ) => Promise<WalletCoreResponseType<Tag>>;
+}
+
+const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
+ [WalletApiOperation.TestingWaitExchangeState]: {
+ codec: codecForAny(),
+ handler: handleTestingWaitExchangeState,
+ },
+ [WalletApiOperation.HintApplicationResumed]: {
+ codec: codecForEmptyObject(),
+ handler: handleHintApplicationResumed,
+ },
+ [WalletApiOperation.AbortTransaction]: {
+ codec: codecForAbortTransaction(),
+ handler: handleAbortTransaction,
+ },
+ [WalletApiOperation.CreateStoredBackup]: {
+ codec: codecForEmptyObject(),
+ handler: handleCreateStoredBackup,
+ },
+ [WalletApiOperation.DeleteStoredBackup]: {
+ codec: codecForDeleteStoredBackupRequest(),
+ handler: handleDeleteStoredBackup,
+ },
+ [WalletApiOperation.ListStoredBackups]: {
+ codec: codecForEmptyObject(),
+ handler: listStoredBackups,
+ },
+ [WalletApiOperation.SetWalletRunConfig]: {
+ codec: codecForInitRequest(),
+ handler: handleSetWalletRunConfig,
+ },
+ // Alias for SetWalletRunConfig
+ [WalletApiOperation.InitWallet]: {
+ codec: codecForInitRequest(),
+ handler: handleSetWalletRunConfig,
+ },
+ [WalletApiOperation.RecoverStoredBackup]: {
+ codec: codecForRecoverStoredBackupRequest(),
+ handler: handleRecoverStoredBackup,
+ },
+ [WalletApiOperation.WithdrawTestkudos]: {
+ codec: codecForEmptyObject(),
+ handler: handleWithdrawTestkudos,
+ },
+ [WalletApiOperation.WithdrawTestBalance]: {
+ codec: codecForWithdrawTestBalance(),
+ handler: handleWithdrawTestBalance,
+ },
+ [WalletApiOperation.RunIntegrationTest]: {
+ codec: codecForIntegrationTestArgs(),
+ handler: handleRunIntegrationTest,
+ },
+ [WalletApiOperation.RunIntegrationTestV2]: {
+ codec: codecForIntegrationTestV2Args(),
+ handler: handleRunIntegrationTestV2,
+ },
+ [WalletApiOperation.ValidateIban]: {
+ codec: codecForValidateIbanRequest(),
+ handler: handleValidateIban,
+ },
+ [WalletApiOperation.TestPay]: {
+ codec: codecForTestPayArgs(),
+ handler: testPay,
+ },
+ [WalletApiOperation.GetTransactions]: {
+ codec: codecForTransactionsRequest(),
+ handler: getTransactions,
+ },
+ [WalletApiOperation.GetTransactionById]: {
+ codec: codecForTransactionByIdRequest(),
+ handler: getTransactionById,
+ },
+ [WalletApiOperation.AddExchange]: {
+ codec: codecForAddExchangeRequest(),
+ handler: handleAddExchange,
+ },
+ [WalletApiOperation.TestingPing]: {
+ codec: codecForEmptyObject(),
+ handler: async () => ({}),
+ },
+ [WalletApiOperation.UpdateExchangeEntry]: {
+ codec: codecForUpdateExchangeEntryRequest(),
+ handler: handleUpdateExchangeEntry,
+ },
+ [WalletApiOperation.TestingGetDenomStats]: {
+ codec: codecForTestingGetDenomStatsRequest(),
+ handler: handleTestingGetDenomStats,
+ },
+ [WalletApiOperation.ListExchanges]: {
+ codec: codecForEmptyObject(),
+ handler: listExchanges,
+ },
+ [WalletApiOperation.GetExchangeEntryByUrl]: {
+ codec: codecForGetExchangeEntryByUrlRequest(),
+ handler: lookupExchangeByUri,
+ },
+ [WalletApiOperation.ListExchangesForScopedCurrency]: {
+ codec: codecForListExchangesForScopedCurrencyRequest(),
+ handler: handleListExchangesForScopedCurrency,
+ },
+ [WalletApiOperation.GetExchangeDetailedInfo]: {
+ codec: codecForAddExchangeRequest(),
+ handler: (wex, req) => getExchangeDetailedInfo(wex, req.exchangeBaseUrl),
+ },
+ [WalletApiOperation.ListKnownBankAccounts]: {
+ codec: codecForListKnownBankAccounts(),
+ handler: handleListKnownBankAccounts,
+ },
+ [WalletApiOperation.AddKnownBankAccounts]: {
+ codec: codecForAddKnownBankAccounts(),
+ handler: handleAddKnownBankAccount,
+ },
+ [WalletApiOperation.ForgetKnownBankAccounts]: {
+ codec: codecForForgetKnownBankAccounts(),
+ handler: handleForgetKnownBankAccounts,
+ },
+ [WalletApiOperation.GetWithdrawalDetailsForUri]: {
+ codec: codecForGetWithdrawalDetailsForUri(),
+ handler: (wex, req) =>
+ getWithdrawalDetailsForUri(wex, req.talerWithdrawUri),
+ },
+ [WalletApiOperation.TestingGetReserveHistory]: {
+ codec: codecForTestingGetReserveHistoryRequest(),
+ handler: handleTestingGetReserveHistory,
+ },
+ [WalletApiOperation.AcceptManualWithdrawal]: {
+ codec: codecForAcceptManualWithdrawalRequest(),
+ handler: handleAcceptManualWithdrawal,
+ },
+ [WalletApiOperation.GetWithdrawalDetailsForAmount]: {
+ codec: codecForGetWithdrawalDetailsForAmountRequest(),
+ handler: getWithdrawalDetailsForAmount,
+ },
+ [WalletApiOperation.GetBalances]: {
+ codec: codecForEmptyObject(),
+ handler: getBalances,
+ },
+ [WalletApiOperation.GetBalanceDetail]: {
+ codec: codecForGetBalanceDetailRequest(),
+ handler: getBalanceDetail,
+ },
+ [WalletApiOperation.GetUserAttentionRequests]: {
+ codec: codecForUserAttentionsRequest(),
+ handler: getUserAttentions,
+ },
+ [WalletApiOperation.MarkAttentionRequestAsRead]: {
+ codec: codecForUserAttentionByIdRequest(),
+ handler: async (wex, req) => {
+ await markAttentionRequestAsRead(wex, req);
return {};
- }
- case WalletApiOperation.SuspendTransaction: {
- const req = codecForSuspendTransaction().decode(payload);
- await suspendTransaction(wex, req.transactionId);
+ },
+ },
+ [WalletApiOperation.GetUserAttentionUnreadCount]: {
+ codec: codecForUserAttentionsRequest(),
+ handler: getUserAttentionsUnreadCount,
+ },
+ [WalletApiOperation.SetExchangeTosAccepted]: {
+ codec: codecForAcceptExchangeTosRequest(),
+ handler: async (wex, req) => {
+ await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
return {};
- }
- case WalletApiOperation.GetActiveTasks: {
- const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds;
-
- const tasksInfo = await Promise.all(
- allTasksId.map(async (id) => {
- return await wex.db.runReadOnlyTx(
- { storeNames: ["operationRetries"] },
- async (tx) => {
- return tx.operationRetries.get(id);
- },
- );
- }),
- );
-
- const tasks = allTasksId.map((taskId, i): ActiveTask => {
- const transaction = convertTaskToTransactionId(taskId);
- const d = tasksInfo[i];
-
- const firstTry = !d
- ? undefined
- : timestampAbsoluteFromDb(d.retryInfo.firstTry);
- const nextTry = !d
- ? undefined
- : timestampAbsoluteFromDb(d.retryInfo.nextRetry);
- const counter = d?.retryInfo.retryCounter;
- const lastError = d?.lastError;
-
- return {
- taskId: taskId,
- retryCounter: counter,
- firstTry,
- nextTry,
- lastError,
- transaction,
- };
- });
- return { tasks };
- }
- case WalletApiOperation.FailTransaction: {
- const req = codecForFailTransactionRequest().decode(payload);
- await failTransaction(wex, req.transactionId);
+ },
+ },
+ [WalletApiOperation.SetExchangeTosForgotten]: {
+ codec: codecForAcceptExchangeTosRequest(),
+ handler: async (wex, req) => {
+ await forgetExchangeTermsOfService(wex, req.exchangeBaseUrl);
return {};
- }
- case WalletApiOperation.ResumeTransaction: {
- const req = codecForResumeTransaction().decode(payload);
+ },
+ },
+ [WalletApiOperation.AcceptBankIntegratedWithdrawal]: {
+ codec: codecForAcceptBankIntegratedWithdrawalRequest(),
+ handler: handleAcceptBankIntegratedWithdrawal,
+ },
+ [WalletApiOperation.ConfirmWithdrawal]: {
+ codec: codecForConfirmWithdrawalRequestRequest(),
+ handler: confirmWithdrawal,
+ },
+ [WalletApiOperation.PrepareBankIntegratedWithdrawal]: {
+ codec: codecForPrepareBankIntegratedWithdrawalRequest(),
+ handler: prepareBankIntegratedWithdrawal,
+ },
+ [WalletApiOperation.GetExchangeTos]: {
+ codec: codecForGetExchangeTosRequest(),
+ handler: handleGetExchangeTos,
+ },
+ [WalletApiOperation.GetContractTermsDetails]: {
+ codec: codecForGetContractTermsDetails(),
+ handler: handleGetContractTermsDetails,
+ },
+ [WalletApiOperation.RetryPendingNow]: {
+ codec: codecForEmptyObject(),
+ handler: handleRetryPendingNow,
+ },
+ [WalletApiOperation.SharePayment]: {
+ codec: codecForSharePaymentRequest(),
+ handler: handleSharePayment,
+ },
+ [WalletApiOperation.PrepareWithdrawExchange]: {
+ codec: codecForPrepareWithdrawExchangeRequest(),
+ handler: handlePrepareWithdrawExchange,
+ },
+ [WalletApiOperation.CheckPayForTemplate]: {
+ codec: codecForCheckPayTemplateRequest(),
+ handler: checkPayForTemplate,
+ },
+ [WalletApiOperation.PreparePayForUri]: {
+ codec: codecForPreparePayRequest(),
+ handler: (wex, req) => preparePayForUri(wex, req.talerPayUri),
+ },
+ [WalletApiOperation.PreparePayForTemplate]: {
+ codec: codecForPreparePayTemplateRequest(),
+ handler: preparePayForTemplate,
+ },
+ [WalletApiOperation.GetQrCodesForPayto]: {
+ codec: codecForGetQrCodesForPaytoRequest(),
+ handler: handleGetQrCodesForPayto,
+ },
+ [WalletApiOperation.ConfirmPay]: {
+ codec: codecForConfirmPayRequest(),
+ handler: handleConfirmPay,
+ },
+ [WalletApiOperation.SuspendTransaction]: {
+ codec: codecForSuspendTransaction(),
+ handler: handleSuspendTransaction,
+ },
+ [WalletApiOperation.GetActiveTasks]: {
+ codec: codecForEmptyObject(),
+ handler: handleGetActiveTasks,
+ },
+ [WalletApiOperation.FailTransaction]: {
+ codec: codecForFailTransactionRequest(),
+ handler: handleFailTransaction,
+ },
+ [WalletApiOperation.ResumeTransaction]: {
+ codec: codecForResumeTransaction(),
+ handler: async (wex, req) => {
await resumeTransaction(wex, req.transactionId);
return {};
- }
- case WalletApiOperation.DumpCoins: {
- return await dumpCoins(wex);
- }
- case WalletApiOperation.SetCoinSuspended: {
- const req = codecForSetCoinSuspendedRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.DumpCoins]: {
+ codec: codecForEmptyObject(),
+ handler: dumpCoins,
+ },
+ [WalletApiOperation.SetCoinSuspended]: {
+ codec: codecForSetCoinSuspendedRequest(),
+ handler: async (wex, req) => {
await setCoinSuspended(wex, req.coinPub, req.suspended);
return {};
- }
- case WalletApiOperation.TestingGetSampleTransactions:
- return { transactions: sampleWalletCoreTransactions };
- case WalletApiOperation.ForceRefresh: {
- const req = codecForForceRefreshRequest().decode(payload);
- return await forceRefresh(wex, req);
- }
- case WalletApiOperation.StartRefundQueryForUri: {
- const req = codecForPrepareRefundRequest().decode(payload);
- return await startRefundQueryForUri(wex, req.talerRefundUri);
- }
- case WalletApiOperation.StartRefundQuery: {
- const req = codecForStartRefundQueryRequest().decode(payload);
- const txIdParsed = parseTransactionIdentifier(req.transactionId);
- if (!txIdParsed) {
- throw Error("invalid transaction ID");
- }
- if (txIdParsed.tag !== TransactionType.Payment) {
- throw Error("expected payment transaction ID");
- }
- await startQueryRefund(wex, txIdParsed.proposalId);
- return {};
- }
- case WalletApiOperation.AddBackupProvider: {
- const req = codecForAddBackupProviderRequest().decode(payload);
- return await addBackupProvider(wex, req);
- }
- case WalletApiOperation.RunBackupCycle: {
- const req = codecForRunBackupCycle().decode(payload);
+ },
+ },
+ [WalletApiOperation.TestingGetSampleTransactions]: {
+ codec: codecForEmptyObject(),
+ handler: handleTestingGetSampleTransactions,
+ },
+ [WalletApiOperation.StartRefundQueryForUri]: {
+ codec: codecForPrepareRefundRequest(),
+ handler: (wex, req) => startRefundQueryForUri(wex, req.talerRefundUri),
+ },
+ [WalletApiOperation.StartRefundQuery]: {
+ codec: codecForStartRefundQueryRequest(),
+ handler: handleStartRefundQuery,
+ },
+ [WalletApiOperation.AddBackupProvider]: {
+ codec: codecForAddBackupProviderRequest(),
+ handler: addBackupProvider,
+ },
+ [WalletApiOperation.RunBackupCycle]: {
+ codec: codecForRunBackupCycle(),
+ handler: async (wex, req) => {
await runBackupCycle(wex, req);
return {};
- }
- case WalletApiOperation.RemoveBackupProvider: {
- const req = codecForRemoveBackupProvider().decode(payload);
+ },
+ },
+ [WalletApiOperation.RemoveBackupProvider]: {
+ codec: codecForRemoveBackupProvider(),
+ handler: async (wex, req) => {
await removeBackupProvider(wex, req);
return {};
- }
- case WalletApiOperation.ExportBackupRecovery: {
- const resp = await getBackupRecovery(wex);
- return resp;
- }
- case WalletApiOperation.TestingWaitTransactionState: {
- const req = payload as TestingWaitTransactionRequest;
+ },
+ },
+ [WalletApiOperation.ExportBackupRecovery]: {
+ codec: codecForEmptyObject(),
+ handler: getBackupRecovery,
+ },
+ [WalletApiOperation.TestingWaitTransactionState]: {
+ codec: codecForAny(),
+ handler: async (wex, req) => {
await waitTransactionState(wex, req.transactionId, req.txState);
return {};
- }
- case WalletApiOperation.GetCurrencySpecification: {
- // Ignore result, just validate in this mock implementation
- const req = codecForGetCurrencyInfoRequest().decode(payload);
- // Hard-coded mock for KUDOS and TESTKUDOS
- if (req.scope.currency === "KUDOS") {
- const kudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Kudos (Taler Demonstrator)",
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": "ク",
- },
- },
- };
- return kudosResp;
- } else if (req.scope.currency === "TESTKUDOS") {
- const testkudosResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: "Test (Taler Unstable Demonstrator)",
- num_fractional_input_digits: 0,
- num_fractional_normal_digits: 0,
- num_fractional_trailing_zero_digits: 0,
- alt_unit_names: {
- "0": "テ",
- },
- },
- };
- return testkudosResp;
- }
- const defaultResp: GetCurrencySpecificationResponse = {
- currencySpecification: {
- name: req.scope.currency,
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2,
- alt_unit_names: {
- "0": req.scope.currency,
- },
- },
- };
- return defaultResp;
- }
- case WalletApiOperation.ImportBackupRecovery: {
- const req = codecForAny().decode(payload);
+ },
+ },
+ [WalletApiOperation.GetCurrencySpecification]: {
+ codec: codecForGetCurrencyInfoRequest(),
+ handler: handleGetCurrencySpecification,
+ },
+ [WalletApiOperation.ImportBackupRecovery]: {
+ codec: codecForAny(),
+ handler: async (wex, req) => {
await loadBackupRecovery(wex, req);
return {};
- }
- case WalletApiOperation.HintNetworkAvailability: {
- const req = codecForHintNetworkAvailabilityRequest().decode(payload);
- if (req.isNetworkAvailable) {
- await retryAll(wex);
- } else {
- // We're not doing anything right now, but we could stop showing
- // certain errors!
- }
- return {};
- }
- case WalletApiOperation.ConvertDepositAmount: {
- const req = codecForConvertAmountRequest.decode(payload);
- return await convertDepositAmount(wex, req);
- }
- case WalletApiOperation.GetMaxDepositAmount: {
- const req = codecForGetAmountRequest.decode(payload);
- return await getMaxDepositAmount(wex, req);
- }
- case WalletApiOperation.ConvertPeerPushAmount: {
- const req = codecForConvertAmountRequest.decode(payload);
- return await convertPeerPushAmount(wex, req);
- }
- case WalletApiOperation.GetMaxPeerPushAmount: {
- const req = codecForGetAmountRequest.decode(payload);
- return await getMaxPeerPushAmount(wex, req);
- }
- case WalletApiOperation.ConvertWithdrawalAmount: {
- const req = codecForConvertAmountRequest.decode(payload);
- return await convertWithdrawalAmount(wex, req);
- }
- case WalletApiOperation.GetBackupInfo: {
- const resp = await getBackupInfo(wex);
- return resp;
- }
- case WalletApiOperation.PrepareDeposit: {
- const req = codecForPrepareDepositRequest().decode(payload);
- return await checkDepositGroup(wex, req);
- }
- case WalletApiOperation.GenerateDepositGroupTxId:
+ },
+ },
+ [WalletApiOperation.HintNetworkAvailability]: {
+ codec: codecForHintNetworkAvailabilityRequest(),
+ handler: handleHintNetworkAvailability,
+ },
+ [WalletApiOperation.ConvertDepositAmount]: {
+ codec: codecForConvertAmountRequest,
+ handler: convertDepositAmount,
+ },
+ [WalletApiOperation.GetMaxDepositAmount]: {
+ codec: codecForGetMaxDepositAmountRequest,
+ handler: getMaxDepositAmount,
+ },
+ [WalletApiOperation.GetMaxPeerPushDebitAmount]: {
+ codec: codecForGetMaxPeerPushDebitAmountRequest(),
+ handler: getMaxPeerPushDebitAmount,
+ },
+ [WalletApiOperation.GetBackupInfo]: {
+ codec: codecForEmptyObject(),
+ handler: getBackupInfo,
+ },
+ [WalletApiOperation.PrepareDeposit]: {
+ codec: codecForCheckDepositRequest(),
+ handler: checkDepositGroup,
+ },
+ [WalletApiOperation.CheckDeposit]: {
+ codec: codecForCheckDepositRequest(),
+ handler: checkDepositGroup,
+ },
+ [WalletApiOperation.GenerateDepositGroupTxId]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
return {
- transactionId: generateDepositGroupTxId(),
+ transactionId: generateDepositGroupTxId() as TransactionIdStr,
};
- case WalletApiOperation.CreateDepositGroup: {
- const req = codecForCreateDepositGroupRequest().decode(payload);
- return await createDepositGroup(wex, req);
- }
- case WalletApiOperation.DeleteTransaction: {
- const req = codecForDeleteTransactionRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.CreateDepositGroup]: {
+ codec: codecForCreateDepositGroupRequest(),
+ handler: createDepositGroup,
+ },
+ [WalletApiOperation.DeleteTransaction]: {
+ codec: codecForDeleteTransactionRequest(),
+ handler: async (wex, req) => {
await deleteTransaction(wex, req.transactionId);
return {};
- }
- case WalletApiOperation.RetryTransaction: {
- const req = codecForRetryTransactionRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.RetryTransaction]: {
+ codec: codecForRetryTransactionRequest(),
+ handler: async (wex, req) => {
await retryTransaction(wex, req.transactionId);
return {};
- }
- case WalletApiOperation.SetWalletDeviceId: {
- const req = codecForSetWalletDeviceIdRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.SetWalletDeviceId]: {
+ codec: codecForSetWalletDeviceIdRequest(),
+ handler: async (wex, req) => {
await setWalletDeviceId(wex, req.walletDeviceId);
return {};
- }
- case WalletApiOperation.TestCrypto: {
+ },
+ },
+ [WalletApiOperation.TestCrypto]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
return await wex.cryptoApi.hashString({ str: "hello world" });
- }
- case WalletApiOperation.ClearDb: {
- wex.ws.clearAllCaches();
+ },
+ },
+ [WalletApiOperation.ClearDb]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
await clearDatabase(wex.db.idbHandle());
+ wex.ws.clearAllCaches();
return {};
- }
- case WalletApiOperation.Recycle: {
+ },
+ },
+ [WalletApiOperation.Recycle]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
throw Error("not implemented");
- return {};
- }
- case WalletApiOperation.ExportDb: {
+ },
+ },
+ [WalletApiOperation.ExportDb]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
const dbDump = await exportDb(wex.ws.idb);
return dbDump;
- }
- case WalletApiOperation.ListGlobalCurrencyExchanges: {
- const resp: ListGlobalCurrencyExchangesResponse = {
- exchanges: [],
- };
- await wex.db.runReadOnlyTx(
- { storeNames: ["globalCurrencyExchanges"] },
- async (tx) => {
- const gceList = await tx.globalCurrencyExchanges.iter().toArray();
- for (const gce of gceList) {
- resp.exchanges.push({
- currency: gce.currency,
- exchangeBaseUrl: gce.exchangeBaseUrl,
- exchangeMasterPub: gce.exchangeMasterPub,
- });
- }
- },
- );
- return resp;
- }
- case WalletApiOperation.ListGlobalCurrencyAuditors: {
- const resp: ListGlobalCurrencyAuditorsResponse = {
- auditors: [],
- };
- await wex.db.runReadOnlyTx(
- { storeNames: ["globalCurrencyAuditors"] },
- async (tx) => {
- const gcaList = await tx.globalCurrencyAuditors.iter().toArray();
- for (const gca of gcaList) {
- resp.auditors.push({
- currency: gca.currency,
- auditorBaseUrl: gca.auditorBaseUrl,
- auditorPub: gca.auditorPub,
- });
- }
- },
- );
- return resp;
- }
- case WalletApiOperation.AddGlobalCurrencyExchange: {
- const req = codecForAddGlobalCurrencyExchangeRequest().decode(payload);
- await wex.db.runReadWriteTx(
- { storeNames: ["globalCurrencyExchanges"] },
- async (tx) => {
- const key = [
- req.currency,
- req.exchangeBaseUrl,
- req.exchangeMasterPub,
- ];
- const existingRec =
- await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (existingRec) {
- return;
- }
- wex.ws.exchangeCache.clear();
- await tx.globalCurrencyExchanges.add({
- currency: req.currency,
- exchangeBaseUrl: req.exchangeBaseUrl,
- exchangeMasterPub: req.exchangeMasterPub,
- });
- },
- );
- return {};
- }
- case WalletApiOperation.RemoveGlobalCurrencyExchange: {
- const req = codecForRemoveGlobalCurrencyExchangeRequest().decode(payload);
- await wex.db.runReadWriteTx(
- { storeNames: ["globalCurrencyExchanges"] },
- async (tx) => {
- const key = [
- req.currency,
- req.exchangeBaseUrl,
- req.exchangeMasterPub,
- ];
- const existingRec =
- await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (!existingRec) {
- return;
- }
- wex.ws.exchangeCache.clear();
- checkDbInvariant(!!existingRec.id, `no global exchange for ${j2s(key)}`);
- await tx.globalCurrencyExchanges.delete(existingRec.id);
- },
- );
- return {};
- }
- case WalletApiOperation.AddGlobalCurrencyAuditor: {
- const req = codecForAddGlobalCurrencyAuditorRequest().decode(payload);
- await wex.db.runReadWriteTx(
- { storeNames: ["globalCurrencyAuditors"] },
- async (tx) => {
- const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
- const existingRec =
- await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (existingRec) {
- return;
- }
- await tx.globalCurrencyAuditors.add({
- currency: req.currency,
- auditorBaseUrl: req.auditorBaseUrl,
- auditorPub: req.auditorPub,
- });
- wex.ws.exchangeCache.clear();
- },
- );
- return {};
- }
- case WalletApiOperation.TestingWaitTasksDone: {
+ },
+ },
+ [WalletApiOperation.GetDepositWireTypesForCurrency]: {
+ codec: codecForGetDepositWireTypesForCurrencyRequest(),
+ handler: handleGetDepositWireTypesForCurrency,
+ },
+ [WalletApiOperation.ListGlobalCurrencyExchanges]: {
+ codec: codecForEmptyObject(),
+ handler: handleListGlobalCurrencyExchanges,
+ },
+ [WalletApiOperation.ListGlobalCurrencyAuditors]: {
+ codec: codecForEmptyObject(),
+ handler: handleListGlobalCurrencyAuditors,
+ },
+ [WalletApiOperation.AddGlobalCurrencyExchange]: {
+ codec: codecForAddGlobalCurrencyExchangeRequest(),
+ handler: handleAddGlobalCurrencyExchange,
+ },
+ [WalletApiOperation.RemoveGlobalCurrencyExchange]: {
+ codec: codecForRemoveGlobalCurrencyExchangeRequest(),
+ handler: handleRemoveGlobalCurrencyExchange,
+ },
+ [WalletApiOperation.AddGlobalCurrencyAuditor]: {
+ codec: codecForAddGlobalCurrencyAuditorRequest(),
+ handler: handleAddGlobalCurrencyAuditor,
+ },
+ [WalletApiOperation.TestingWaitTasksDone]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
await waitTasksDone(wex);
return {};
- }
- case WalletApiOperation.TestingResetAllRetries:
+ },
+ },
+ [WalletApiOperation.TestingResetAllRetries]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
await retryAll(wex);
return {};
- case WalletApiOperation.RemoveGlobalCurrencyAuditor: {
- const req = codecForRemoveGlobalCurrencyAuditorRequest().decode(payload);
- await wex.db.runReadWriteTx(
- { storeNames: ["globalCurrencyAuditors"] },
- async (tx) => {
- const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
- const existingRec =
- await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (!existingRec) {
- return;
- }
- checkDbInvariant(!!existingRec.id, `no global currency for ${j2s(key)}`);
- await tx.globalCurrencyAuditors.delete(existingRec.id);
- wex.ws.exchangeCache.clear();
- },
- );
- return {};
- }
- case WalletApiOperation.ImportDb: {
- const req = codecForImportDbRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.RemoveGlobalCurrencyAuditor]: {
+ codec: codecForRemoveGlobalCurrencyAuditorRequest(),
+ handler: handleRemoveGlobalCurrencyAuditor,
+ },
+ [WalletApiOperation.ImportDb]: {
+ codec: codecForImportDbRequest(),
+ handler: async (wex, req) => {
+ // FIXME: This should atomically re-materialize transactions!
await importDb(wex.db.idbHandle(), req.dump);
- return [];
- }
- case WalletApiOperation.CheckPeerPushDebit: {
- const req = codecForCheckPeerPushDebitRequest().decode(payload);
- return await checkPeerPushDebit(wex, req);
- }
- case WalletApiOperation.InitiatePeerPushDebit: {
- const req = codecForInitiatePeerPushDebitRequest().decode(payload);
- return await initiatePeerPushDebit(wex, req);
- }
- case WalletApiOperation.PreparePeerPushCredit: {
- const req = codecForPreparePeerPushCreditRequest().decode(payload);
- return await preparePeerPushCredit(wex, req);
- }
- case WalletApiOperation.ConfirmPeerPushCredit: {
- const req = codecForConfirmPeerPushPaymentRequest().decode(payload);
- return await confirmPeerPushCredit(wex, req);
- }
- case WalletApiOperation.CheckPeerPullCredit: {
- const req = codecForPreparePeerPullPaymentRequest().decode(payload);
- return await checkPeerPullPaymentInitiation(wex, req);
- }
- case WalletApiOperation.InitiatePeerPullCredit: {
- const req = codecForInitiatePeerPullPaymentRequest().decode(payload);
- return await initiatePeerPullPayment(wex, req);
- }
- case WalletApiOperation.PreparePeerPullDebit: {
- const req = codecForCheckPeerPullPaymentRequest().decode(payload);
- return await preparePeerPullDebit(wex, req);
- }
- case WalletApiOperation.ConfirmPeerPullDebit: {
- const req = codecForAcceptPeerPullPaymentRequest().decode(payload);
- return await confirmPeerPullDebit(wex, req);
- }
- case WalletApiOperation.ApplyDevExperiment: {
- const req = codecForApplyDevExperiment().decode(payload);
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ await rematerializeTransactions(wex, tx);
+ });
+ return {};
+ },
+ },
+ [WalletApiOperation.CheckPeerPushDebit]: {
+ codec: codecForCheckPeerPushDebitRequest(),
+ handler: checkPeerPushDebit,
+ },
+ [WalletApiOperation.InitiatePeerPushDebit]: {
+ codec: codecForInitiatePeerPushDebitRequest(),
+ handler: initiatePeerPushDebit,
+ },
+ [WalletApiOperation.PreparePeerPushCredit]: {
+ codec: codecForPreparePeerPushCreditRequest(),
+ handler: preparePeerPushCredit,
+ },
+ [WalletApiOperation.ConfirmPeerPushCredit]: {
+ codec: codecForConfirmPeerPushPaymentRequest(),
+ handler: confirmPeerPushCredit,
+ },
+ [WalletApiOperation.CheckPeerPullCredit]: {
+ codec: codecForPreparePeerPullPaymentRequest(),
+ handler: checkPeerPullCredit,
+ },
+ [WalletApiOperation.InitiatePeerPullCredit]: {
+ codec: codecForInitiatePeerPullPaymentRequest(),
+ handler: initiatePeerPullPayment,
+ },
+ [WalletApiOperation.PreparePeerPullDebit]: {
+ codec: codecForCheckPeerPullPaymentRequest(),
+ handler: preparePeerPullDebit,
+ },
+ [WalletApiOperation.ConfirmPeerPullDebit]: {
+ codec: codecForAcceptPeerPullPaymentRequest(),
+ handler: confirmPeerPullDebit,
+ },
+ [WalletApiOperation.ApplyDevExperiment]: {
+ codec: codecForApplyDevExperiment(),
+ handler: async (wex, req) => {
await applyDevExperiment(wex, req.devExperimentUri);
return {};
- }
- case WalletApiOperation.Shutdown: {
- wex.ws.stop();
+ },
+ },
+ [WalletApiOperation.Shutdown]: {
+ codec: codecForEmptyObject(),
+ handler: handleShutdown,
+ },
+ [WalletApiOperation.GetVersion]: {
+ codec: codecForEmptyObject(),
+ handler: handleGetVersion,
+ },
+ [WalletApiOperation.TestingWaitTransactionsFinal]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
+ await waitUntilAllTransactionsFinal(wex);
return {};
- }
- case WalletApiOperation.GetVersion: {
- return getVersion(wex);
- }
- case WalletApiOperation.TestingWaitTransactionsFinal:
- return await waitUntilAllTransactionsFinal(wex);
- case WalletApiOperation.TestingWaitRefreshesFinal:
- return await waitUntilRefreshesDone(wex);
- case WalletApiOperation.TestingSetTimetravel: {
- const req = codecForTestingSetTimetravelRequest().decode(payload);
- setDangerousTimetravel(req.offsetMs);
- await wex.taskScheduler.reload();
+ },
+ },
+ [WalletApiOperation.TestingWaitRefreshesFinal]: {
+ codec: codecForEmptyObject(),
+ handler: async (wex, req) => {
+ await waitUntilRefreshesDone(wex);
return {};
- }
- case WalletApiOperation.DeleteExchange: {
- const req = codecForDeleteExchangeRequest().decode(payload);
- await deleteExchange(wex, req);
+ },
+ },
+ [WalletApiOperation.TestingSetTimetravel]: {
+ codec: codecForTestingSetTimetravelRequest(),
+ handler: async (wex, req) => {
+ await handleTestingSetTimetravel(wex, req);
return {};
- }
- case WalletApiOperation.GetExchangeResources: {
- const req = codecForGetExchangeResourcesRequest().decode(payload);
+ },
+ },
+ [WalletApiOperation.DeleteExchange]: {
+ codec: codecForDeleteExchangeRequest(),
+ handler: handleDeleteExchange,
+ },
+ [WalletApiOperation.GetExchangeResources]: {
+ codec: codecForGetExchangeResourcesRequest(),
+ handler: async (wex, req) => {
return await getExchangeResources(wex, req.exchangeBaseUrl);
- }
- case WalletApiOperation.CanonicalizeBaseUrl: {
- const req = codecForCanonicalizeBaseUrlRequest().decode(payload);
- return {
- url: canonicalizeBaseUrl(req.url),
- };
- }
- case WalletApiOperation.TestingInfiniteTransactionLoop: {
- const myDelayMs = (payload as any).delayMs ?? 5;
- const shouldFetch = !!(payload as any).shouldFetch;
- const doFetch = async () => {
- while (1) {
- const url =
- "https://exchange.demo.taler.net/reserves/01PMMB9PJN0QBWAFBXV6R0KNJJMAKXCV4D6FDG0GJFDJQXGYP32G?timeout_ms=30000";
- logger.info(`fetching ${url}`);
- const res = await wex.http.fetch(url);
- logger.info(`fetch result ${res.status}`);
- }
- };
- if (shouldFetch) {
- // In the background!
- doFetch();
- }
- let loopCount = 0;
- while (true) {
- logger.info(`looping test write tx, iteration ${loopCount}`);
- await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
- await tx.config.put({
- key: ConfigRecordKey.TestLoopTx,
- value: loopCount,
- });
- });
- if (myDelayMs != 0) {
- await new Promise<void>((resolve, reject) => {
- setTimeout(() => resolve(), myDelayMs);
- });
- }
- loopCount = (loopCount + 1) % (Number.MAX_SAFE_INTEGER - 1);
- }
- }
- // default:
- // assertUnreachable(operation);
- }
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
- {
- operation,
},
- "unknown operation",
- );
-}
+ },
+ [WalletApiOperation.CanonicalizeBaseUrl]: {
+ codec: codecForCanonicalizeBaseUrlRequest(),
+ handler: handleCanonicalizeBaseUrl,
+ },
+ [WalletApiOperation.ForceRefresh]: {
+ codec: codecForForceRefreshRequest(),
+ handler: async (wex, req) => {
+ await forceRefresh(wex, req);
+ return {};
+ },
+ },
+ [WalletApiOperation.ExportBackup]: {
+ codec: codecForAny(),
+ handler: async (wex, req) => {
+ throw Error("not implemented");
+ },
+ },
+ [WalletApiOperation.ListAssociatedRefreshes]: {
+ codec: codecForAny(),
+ handler: async (wex, req) => {
+ throw Error("not implemented");
+ },
+ },
+ [WalletApiOperation.GetBankingChoicesForPayto]: {
+ codec: codecForGetBankingChoicesForPaytoRequest(),
+ handler: handleGetBankingChoicesForPayto,
+ },
+ [WalletApiOperation.StartExchangeWalletKyc]: {
+ codec: codecForStartExchangeWalletKycRequest(),
+ handler: handleStartExchangeWalletKyc,
+ },
+ [WalletApiOperation.TestingWaitExchangeWalletKyc]: {
+ codec: codecForTestingWaitWalletKycRequest(),
+ handler: handleTestingWaitExchangeWalletKyc,
+ },
+};
-export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
- const result: WalletCoreVersion = {
- implementationSemver: walletCoreBuildInfo.implementationSemver,
- implementationGitHash: walletCoreBuildInfo.implementationGitHash,
- hash: undefined,
- version: WALLET_CORE_API_PROTOCOL_VERSION,
- exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
- merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
- bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
- bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- devMode: wex.ws.config.testing.devModeActive,
- };
- return result;
+/**
+ * Implementation of the "wallet-core" API.
+ */
+async function dispatchRequestInternal(
+ wex: WalletExecutionContext,
+ operation: WalletApiOperation,
+ payload: unknown,
+): Promise<WalletCoreResponseType<typeof operation>> {
+ if (!wex.ws.initCalled && operation !== WalletApiOperation.InitWallet) {
+ throw Error(
+ `wallet must be initialized before running operation ${operation}`,
+ );
+ }
+
+ const h: HandlerWithValidator<any> = handlers[operation];
+
+ if (!h) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
+ {
+ operation,
+ },
+ "unknown operation",
+ );
+ }
+
+ const req = h.codec.decode(payload);
+ return await h.handler(wex, req);
}
export function getObservedWalletExecutionContext(
ws: InternalWalletState,
cancellationToken: CancellationToken,
+ cts: CancellationToken.Source | undefined,
oc: ObservabilityContext,
): WalletExecutionContext {
+ const db = ws.createDbAccessHandle(cancellationToken);
const wex: WalletExecutionContext = {
ws,
cancellationToken,
+ cts,
cryptoApi: observeTalerCrypto(ws.cryptoApi, oc),
- db: new ObservableDbAccess(ws.db, oc),
+ db: new ObservableDbAccess(db, oc),
http: new ObservableHttpClientLibrary(ws.http, oc),
taskScheduler: new ObservableTaskScheduler(ws.taskScheduler, oc),
oc,
@@ -1608,13 +2196,16 @@ export function getObservedWalletExecutionContext(
export function getNormalWalletExecutionContext(
ws: InternalWalletState,
cancellationToken: CancellationToken,
+ cts: CancellationToken.Source | undefined,
oc: ObservabilityContext,
): WalletExecutionContext {
+ const db = ws.createDbAccessHandle(cancellationToken);
const wex: WalletExecutionContext = {
ws,
cancellationToken,
+ cts,
cryptoApi: ws.cryptoApi,
- db: ws.db,
+ db,
get http() {
if (ws.initCalled) {
return ws.http;
@@ -1630,7 +2221,7 @@ export function getNormalWalletExecutionContext(
/**
* Handle a request to the wallet-core API.
*/
-async function handleCoreApiRequest(
+async function dispatchWalletCoreApiRequest(
ws: InternalWalletState,
operation: string,
id: string,
@@ -1661,12 +2252,12 @@ async function handleCoreApiRequest(
},
};
- wex = getObservedWalletExecutionContext(ws, cts.token, oc);
+ wex = getObservedWalletExecutionContext(ws, cts.token, cts, oc);
} else {
oc = {
observe(evt) {},
};
- wex = getNormalWalletExecutionContext(ws, cts.token, oc);
+ wex = getNormalWalletExecutionContext(ws, cts.token, cts, oc);
}
try {
@@ -1677,7 +2268,6 @@ async function handleCoreApiRequest(
});
const result = await dispatchRequestInternal(
wex,
- cts,
operation as any,
payload,
);
@@ -1709,9 +2299,7 @@ async function handleCoreApiRequest(
}
}
-export function applyRunConfigDefaults(
- wcp?: PartialWalletRunConfig,
-): WalletRunConfig {
+function applyRunConfigDefaults(wcp?: PartialWalletRunConfig): WalletRunConfig {
return {
builtin: {
exchanges: wcp?.builtin?.exchanges ?? [
@@ -1787,7 +2375,7 @@ export class Wallet {
payload: unknown,
): Promise<CoreApiResponse> {
await this.ws.ensureWalletDbOpen();
- return handleCoreApiRequest(this.ws, operation, id, payload);
+ return dispatchWalletCoreApiRequest(this.ws, operation, id, payload);
}
}
@@ -1861,6 +2449,16 @@ class WalletDbTriggerSpec implements TriggerSpec {
}
}
+type LongpollRunFn<T> = (timeoutMs: number) => Promise<T>;
+type ResolveFn = () => void;
+
+/**
+ * Per-hostname state for longpolling.
+ */
+interface LongpollState {
+ queue: Array<ResolveFn>;
+}
+
/**
* Internal state of the wallet.
*
@@ -1868,10 +2466,9 @@ class WalletDbTriggerSpec implements TriggerSpec {
*/
export class InternalWalletState {
cryptoApi: TalerCryptoInterface;
- cryptoDispatcher: CryptoDispatcher;
+ private cryptoDispatcher: CryptoDispatcher;
readonly timerGroup: TimerGroup;
- workAvailable = new AsyncCondition();
stopped = false;
private listeners: NotificationListener[] = [];
@@ -1913,6 +2510,14 @@ export class InternalWalletState {
private _http: HttpRequestLibrary | undefined = undefined;
+ devExperimentState: DevExperimentState = {};
+
+ clientCancellationMap: Map<string, CancellationToken.Source> = new Map();
+
+ private longpollStatePerHostname: Map<string, LongpollState> = new Map();
+
+ private longpollRequestIdCounter = 1;
+
get db(): DbAccess<typeof WalletStoresV1> {
if (!this._dbAccessHandle) {
this._dbAccessHandle = this.createDbAccessHandle(
@@ -1922,9 +2527,83 @@ export class InternalWalletState {
return this._dbAccessHandle;
}
- devExperimentState: DevExperimentState = {};
+ /**
+ * When set to false, all tasks that require network will be stopped and
+ * retried until connection is restored.
+ *
+ * Set to true by default for compatibility with clients that don't hint
+ * network availability via hintNetworkAvailability.
+ */
+ private _networkAvailable = true;
- clientCancellationMap: Map<string, CancellationToken.Source> = new Map();
+ get networkAvailable(): boolean {
+ return this._networkAvailable;
+ }
+
+ set networkAvailable(status: boolean) {
+ this._networkAvailable = status;
+ }
+
+ /**
+ * Run a long-polling request, potentially queueing the request
+ * if too many other long-polling requests against the same hostname
+ * (or too many overall) are active.
+ */
+ async runLongpollQueueing<T>(
+ wex: WalletExecutionContext,
+ hostname: string,
+ f: LongpollRunFn<T>,
+ ): Promise<T> {
+ let rid = this.longpollRequestIdCounter++;
+ const triggerNextLongpoll = () => {
+ logger.info(`cleaning up after long-poll ${rid} request to ${hostname}`);
+ const st = this.longpollStatePerHostname.get(hostname);
+ if (!st) {
+ return;
+ }
+ const next = st.queue.shift();
+ if (next) {
+ next();
+ } else {
+ this.longpollStatePerHostname.delete(hostname);
+ }
+ };
+ const doRunLongpoll: () => Promise<T> = async () => {
+ const st = this.longpollStatePerHostname.get(hostname);
+ const numWaiting = st?.queue.length ?? 0;
+ logger.info(
+ `running long-poll ${rid} to ${hostname} with ${numWaiting} waiting`,
+ );
+ try {
+ const timeoutMs = Math.round(Math.max(10000, 30000 / (numWaiting + 1)));
+ return await f(timeoutMs);
+ } finally {
+ triggerNextLongpoll();
+ }
+ };
+ const state = this.longpollStatePerHostname.get(hostname);
+ if (state) {
+ logger.info(`long-poll request ${rid} to ${hostname} queued`);
+ const promcap = openPromise<void>();
+ state.queue.push(promcap.resolve);
+ try {
+ await wex.cancellationToken.racePromise(promcap.promise);
+ } catch (e) {
+ logger.info(
+ `long-poll request ${rid} to ${hostname} cancelled while queued`,
+ );
+ triggerNextLongpoll();
+ throw e;
+ }
+ return doRunLongpoll();
+ } else {
+ logger.info(`directly running long-poll request ${rid} to ${hostname}`);
+ this.longpollStatePerHostname.set(hostname, {
+ queue: [],
+ });
+ return Promise.resolve().then(doRunLongpoll);
+ }
+ }
clearAllCaches(): void {
this.exchangeCache.clear();
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 0434aefc2..e906603a3 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -26,6 +26,7 @@ import {
AbsoluteTime,
AcceptManualWithdrawalResult,
AcceptWithdrawalResponse,
+ AccountKycStatus,
AgeRestriction,
Amount,
AmountJson,
@@ -46,6 +47,7 @@ import {
ExchangeBatchWithdrawRequest,
ExchangeListItem,
ExchangeUpdateStatus,
+ ExchangeWalletKycStatus,
ExchangeWireAccount,
ExchangeWithdrawBatchResponse,
ExchangeWithdrawRequest,
@@ -59,11 +61,13 @@ import {
NotificationType,
ObservabilityEventType,
PrepareBankIntegratedWithdrawalResponse,
+ ScopeInfo,
TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
+ TalerUriAction,
Transaction,
TransactionAction,
TransactionIdStr,
@@ -71,6 +75,7 @@ import {
TransactionMinorState,
TransactionState,
TransactionType,
+ TransactionWithdrawal,
URL,
UnblindedSignature,
WalletNotification,
@@ -80,21 +85,25 @@ import {
WithdrawalType,
addPaytoQueryParams,
assertUnreachable,
+ checkAccountRestriction,
checkDbInvariant,
checkLogicInvariant,
- codeForBankWithdrawalOperationPostResponse,
+ codecForAccountKycStatus,
+ codecForBankWithdrawalOperationPostResponse,
codecForBankWithdrawalOperationStatus,
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeWithdrawBatchResponse,
+ codecForLegitimizationNeededResponse,
codecForReserveStatus,
- codecForWalletKycUuid,
codecForWithdrawOperationStatusResponse,
encodeCrock,
getErrorDetailFromException,
getRandomBytes,
j2s,
makeErrorDetail,
+ parsePaytoUri,
+ parseTalerUri,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
@@ -116,18 +125,23 @@ import {
TransitionResultType,
constructTaskIdentifier,
genericWaitForState,
+ genericWaitForStateVal,
makeCoinAvailable,
makeCoinsVisible,
+ requireExchangeTosAcceptedOrThrow,
+ runWithClientCancellation,
} from "./common.js";
-import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
+import { EddsaKeyPairStrings } from "./crypto/cryptoImplementation.js";
import {
CoinRecord,
CoinSourceType,
DenominationRecord,
DenominationVerificationStatus,
- KycPendingInfo,
+ OperationRetryRecord,
PlanchetRecord,
PlanchetStatus,
+ WalletDbAllStoresReadOnlyTransaction,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletDbStoresArr,
@@ -146,10 +160,15 @@ import {
} from "./denomSelection.js";
import { isWithdrawableDenom } from "./denominations.js";
import {
+ BalanceThresholdCheckResult,
+ ExchangeWireDetails,
ReadyExchangeSummary,
+ checkIncomingAmountLegalUnderKycBalanceThreshold,
fetchFreshExchange,
getExchangePaytoUri,
getExchangeWireDetailsInTx,
+ getScopeForAllExchanges,
+ handleStartExchangeWalletKyc,
listExchanges,
lookupExchangeByUri,
markExchangeUsed,
@@ -162,10 +181,7 @@ import {
notifyTransition,
parseTransactionIdentifier,
} from "./transactions.js";
-import {
- WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- WALLET_EXCHANGE_PROTOCOL_VERSION,
-} from "./versions.js";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
@@ -173,132 +189,139 @@ import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
*/
const logger = new Logger("withdraw.ts");
-/**
- * Update the materialized withdrawal transaction based
- * on the withdrawal group record.
- */
-async function updateWithdrawalTransaction(
- ctx: WithdrawTransactionContext,
- tx: WalletDbReadWriteTransaction<
- [
- "withdrawalGroups",
- "transactions",
- "operationRetries",
- "exchanges",
- "exchangeDetails",
- ]
- >,
-): Promise<void> {
- const wgRecord = await tx.withdrawalGroups.get(ctx.withdrawalGroupId);
- if (!wgRecord) {
- await tx.transactions.delete(ctx.transactionId);
- return;
- }
- const retryRecord = await tx.operationRetries.get(ctx.taskId);
-
- let transactionItem: Transaction;
+interface TxKycDetails {
+ kycAccessToken?: string;
+ kycUrl?: string;
+ kycPaytoHash?: string;
+}
- if (
- !wgRecord.instructedAmount ||
- !wgRecord.denomsSel ||
- !wgRecord.exchangeBaseUrl
- ) {
- // withdrawal group is in preparation, nothing to update
- return;
+function buildTransactionForBankIntegratedWithdraw(
+ wg: WithdrawalGroupRecord,
+ scopes: ScopeInfo[],
+ ort: OperationRetryRecord | undefined,
+ kycDetails: TxKycDetails | undefined,
+): TransactionWithdrawal {
+ if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
+ throw Error("");
+ }
+ const instructedCurrency =
+ wg.instructedAmount === undefined
+ ? undefined
+ : Amounts.currencyOf(wg.instructedAmount);
+ const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
+ checkDbInvariant(
+ currency !== undefined,
+ "wg uninitialized (missing currency)",
+ );
+ const txState = computeWithdrawalTransactionStatus(wg);
+
+ const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
+ let txDetails: TransactionWithdrawal = {
+ type: TransactionType.Withdrawal,
+ txState,
+ scopes,
+ txActions: computeWithdrawalTransactionActions(wg),
+ exchangeBaseUrl: wg.exchangeBaseUrl,
+ amountEffective:
+ isUnsuccessfulTransaction(txState) || !wg.denomsSel
+ ? zero
+ : Amounts.stringify(wg.denomsSel.totalCoinValue),
+ amountRaw: !wg.instructedAmount
+ ? zero
+ : Amounts.stringify(wg.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
+ exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
+ reservePub: wg.reservePub,
+ bankConfirmationUrl: wg.wgInfo.bankInfo.externalConfirmation
+ ? undefined
+ : wg.wgInfo.bankInfo.confirmUrl,
+ externalConfirmation: wg.wgInfo.bankInfo.externalConfirmation,
+ reserveIsReady:
+ wg.status === WithdrawalGroupStatus.Done ||
+ wg.status === WithdrawalGroupStatus.PendingReady,
+ },
+ kycUrl: kycDetails?.kycUrl,
+ kycAccessToken: wg.kycAccessToken,
+ kycPaytoHash: wg.kycPaytoHash,
+ abortReason: wg.abortReason,
+ failReason: wg.failReason,
+ timestamp: timestampPreciseFromDb(wg.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: wg.withdrawalGroupId,
+ }),
+ };
+ if (ort?.lastError) {
+ txDetails.error = ort.lastError;
}
+ if (kycDetails) {
+ txDetails = { ...txDetails, ...kycDetails };
+ }
+ return txDetails;
+}
- if (wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated) {
- const txState = computeWithdrawalTransactionStatus(wgRecord);
- transactionItem = {
- type: TransactionType.Withdrawal,
- txState,
- txActions: computeWithdrawalTransactionActions(wgRecord),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount))
- : Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wgRecord.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.TalerBankIntegrationApi,
- confirmed: wgRecord.wgInfo.bankInfo.timestampBankConfirmed
- ? true
- : false,
- exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts,
- reservePub: wgRecord.reservePub,
- bankConfirmationUrl: wgRecord.wgInfo.bankInfo.confirmUrl,
- reserveIsReady:
- wgRecord.status === WithdrawalGroupStatus.Done ||
- wgRecord.status === WithdrawalGroupStatus.PendingReady,
- },
- kycUrl: wgRecord.kycUrl,
- exchangeBaseUrl: wgRecord.exchangeBaseUrl,
- timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
- transactionId: ctx.transactionId,
- };
- } else if (
- wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual
- ) {
- checkDbInvariant(
- wgRecord.instructedAmount !== undefined,
- "manual withdrawal without amount can't be created",
- );
- checkDbInvariant(
- wgRecord.denomsSel !== undefined,
- "manual withdrawal without denoms can't be created",
- );
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- wgRecord.exchangeBaseUrl,
- );
- const plainPaytoUris =
- exchangeDetails?.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+function buildTransactionForManualWithdraw(
+ wg: WithdrawalGroupRecord,
+ exchangeDetails: ExchangeWireDetails | undefined,
+ scopes: ScopeInfo[],
+ ort: OperationRetryRecord | undefined,
+ kycDetails: TxKycDetails | undefined,
+): TransactionWithdrawal {
+ if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.BankManual)
+ throw Error("");
- const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
- plainPaytoUris,
- wgRecord.reservePub,
- wgRecord.instructedAmount,
- );
+ const plainPaytoUris =
+ exchangeDetails?.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
- const txState = computeWithdrawalTransactionStatus(wgRecord);
-
- transactionItem = {
- type: TransactionType.Withdrawal,
- txState,
- txActions: computeWithdrawalTransactionActions(wgRecord),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(wgRecord.instructedAmount))
- : Amounts.stringify(wgRecord.denomsSel.totalCoinValue),
- amountRaw: Amounts.stringify(wgRecord.instructedAmount),
- withdrawalDetails: {
- type: WithdrawalType.ManualTransfer,
- reservePub: wgRecord.reservePub,
- exchangePaytoUris,
- exchangeCreditAccountDetails: wgRecord.wgInfo.exchangeCreditAccounts,
- reserveIsReady:
- wgRecord.status === WithdrawalGroupStatus.Done ||
- wgRecord.status === WithdrawalGroupStatus.PendingReady,
- },
- kycUrl: wgRecord.kycUrl,
- exchangeBaseUrl: wgRecord.exchangeBaseUrl,
- timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
- transactionId: ctx.transactionId,
- };
- } else {
- // FIXME: If this is an orphaned withdrawal for a p2p transaction, we
- // still might want to report the withdrawal.
- return;
+ checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
+ checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");
+ const exchangePaytoUris = augmentPaytoUrisForWithdrawal(
+ plainPaytoUris,
+ wg.reservePub,
+ wg.instructedAmount,
+ );
+
+ const txState = computeWithdrawalTransactionStatus(wg);
+
+ let txDetails: TransactionWithdrawal = {
+ type: TransactionType.Withdrawal,
+ txState,
+ scopes,
+ txActions: computeWithdrawalTransactionActions(wg),
+ amountEffective: isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount))
+ : Amounts.stringify(wg.denomsSel.totalCoinValue),
+ amountRaw: Amounts.stringify(wg.instructedAmount),
+ withdrawalDetails: {
+ type: WithdrawalType.ManualTransfer,
+ reservePub: wg.reservePub,
+ exchangePaytoUris,
+ exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
+ reserveIsReady:
+ wg.status === WithdrawalGroupStatus.Done ||
+ wg.status === WithdrawalGroupStatus.PendingReady,
+ reserveClosingDelay: exchangeDetails?.reserveClosingDelay ?? { d_us: 0 },
+ },
+ kycUrl: kycDetails?.kycUrl,
+ exchangeBaseUrl: wg.exchangeBaseUrl,
+ timestamp: timestampPreciseFromDb(wg.timestampStart),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: wg.withdrawalGroupId,
+ }),
+ abortReason: wg.abortReason,
+ ...(ort?.lastError ? { error: ort.lastError } : {}),
+ };
+ if (ort?.lastError) {
+ txDetails.error = ort.lastError;
}
-
- if (retryRecord?.lastError) {
- transactionItem.error = retryRecord.lastError;
+ if (kycDetails) {
+ txDetails = { ...txDetails, ...kycDetails };
}
-
- await tx.transactions.put({
- currency: Amounts.currencyOf(wgRecord.instructedAmount),
- transactionItem,
- exchanges: [wgRecord.exchangeBaseUrl],
- });
-
- // FIXME: Handle orphaned withdrawals where the p2p or recoup tx was deleted?
+ return txDetails;
}
export class WithdrawTransactionContext implements TransactionContext {
@@ -320,6 +343,128 @@ export class WithdrawTransactionContext implements TransactionContext {
}
/**
+ * Get the full transaction details for the transaction.
+ *
+ * Returns undefined if the transaction is in a state where we do not have a
+ * transaction item (e.g. if it was deleted).
+ */
+ async lookupFullTransaction(
+ tx: WalletDbAllStoresReadOnlyTransaction,
+ ): Promise<Transaction | undefined> {
+ const withdrawalGroupRecord = await tx.withdrawalGroups.get(
+ this.withdrawalGroupId,
+ );
+ if (!withdrawalGroupRecord) {
+ return undefined;
+ }
+ const exchangeBaseUrl = withdrawalGroupRecord.exchangeBaseUrl;
+ const exchangeDetails =
+ exchangeBaseUrl == null
+ ? undefined
+ : await getExchangeWireDetailsInTx(tx, exchangeBaseUrl);
+ const scopes = await getScopeForAllExchanges(
+ tx,
+ !exchangeDetails ? [] : [exchangeDetails.exchangeBaseUrl],
+ );
+
+ let kycDetails: TxKycDetails | undefined = undefined;
+
+ if (exchangeBaseUrl) {
+ switch (withdrawalGroupRecord.status) {
+ case WithdrawalGroupStatus.PendingKyc:
+ case WithdrawalGroupStatus.SuspendedKyc: {
+ kycDetails = {
+ kycAccessToken: withdrawalGroupRecord.kycAccessToken,
+ kycPaytoHash: withdrawalGroupRecord.kycPaytoHash,
+ kycUrl: new URL(
+ `kyc-spa/${withdrawalGroupRecord.kycAccessToken}`,
+ exchangeBaseUrl,
+ ).href,
+ };
+ break;
+ }
+ }
+ }
+
+ const ort = await tx.operationRetries.get(this.taskId);
+ if (
+ withdrawalGroupRecord.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ return buildTransactionForBankIntegratedWithdraw(
+ withdrawalGroupRecord,
+ scopes,
+ ort,
+ kycDetails,
+ );
+ }
+ if (!exchangeDetails) {
+ logger.warn(
+ `transaction ${this.transactionId} is a manual withdrawal, but no exchange wire details found`,
+ );
+ }
+ return buildTransactionForManualWithdraw(
+ withdrawalGroupRecord,
+ exchangeDetails,
+ scopes,
+ ort,
+ kycDetails,
+ );
+ }
+
+ /**
+ * Update the metadata of the transaction in the database.
+ */
+ async updateTransactionMeta(
+ tx: WalletDbReadWriteTransaction<["withdrawalGroups", "transactionsMeta"]>,
+ ): Promise<void> {
+ const ctx = this;
+ const wgRecord = await tx.withdrawalGroups.get(ctx.withdrawalGroupId);
+ if (!wgRecord) {
+ await tx.transactionsMeta.delete(ctx.transactionId);
+ return;
+ }
+
+ if (
+ !wgRecord.instructedAmount ||
+ !wgRecord.denomsSel ||
+ !wgRecord.exchangeBaseUrl
+ ) {
+ // withdrawal group is in preparation, nothing to update
+ return;
+ }
+
+ if (
+ wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated
+ ) {
+ } else if (
+ wgRecord.wgInfo.withdrawalType === WithdrawalRecordType.BankManual
+ ) {
+ checkDbInvariant(
+ wgRecord.instructedAmount !== undefined,
+ "manual withdrawal without amount can't be created",
+ );
+ checkDbInvariant(
+ wgRecord.denomsSel !== undefined,
+ "manual withdrawal without denoms can't be created",
+ );
+ } else {
+ // FIXME: If this is an orphaned withdrawal for a p2p transaction, we
+ // still might want to report the withdrawal.
+ return;
+ }
+ await tx.transactionsMeta.put({
+ transactionId: ctx.transactionId,
+ status: wgRecord.status,
+ timestamp: wgRecord.timestampStart,
+ currency: Amounts.currencyOf(wgRecord.instructedAmount),
+ exchanges: [wgRecord.exchangeBaseUrl],
+ });
+
+ // FIXME: Handle orphaned withdrawals where the p2p or recoup tx was deleted?
+ }
+
+ /**
* Transition a withdrawal transaction.
* Extra object stores may be accessed during the transition.
*/
@@ -330,7 +475,7 @@ export class WithdrawTransactionContext implements TransactionContext {
tx: WalletDbReadWriteTransaction<
[
"withdrawalGroups",
- "transactions",
+ "transactionsMeta",
"operationRetries",
"exchanges",
"exchangeDetails",
@@ -341,7 +486,7 @@ export class WithdrawTransactionContext implements TransactionContext {
): Promise<TransitionInfo | undefined> {
const baseStores = [
"withdrawalGroups" as const,
- "transactions" as const,
+ "transactionsMeta" as const,
"operationRetries" as const,
"exchanges" as const,
"exchangeDetails" as const,
@@ -373,11 +518,10 @@ export class WithdrawTransactionContext implements TransactionContext {
return undefined;
}
- // const res = await f(wgRec, tx);
switch (res.type) {
case TransitionResultType.Transition: {
await tx.withdrawalGroups.put(res.rec);
- await updateWithdrawalTransaction(this, tx);
+ await this.updateTransactionMeta(tx);
const newTxState = computeWithdrawalTransactionStatus(res.rec);
return {
oldTxState,
@@ -386,7 +530,7 @@ export class WithdrawTransactionContext implements TransactionContext {
}
case TransitionResultType.Delete:
await tx.withdrawalGroups.delete(this.withdrawalGroupId);
- await updateWithdrawalTransaction(this, tx);
+ await this.updateTransactionMeta(tx);
return {
oldTxState,
newTxState: {
@@ -457,9 +601,6 @@ export class WithdrawTransactionContext implements TransactionContext {
case WithdrawalGroupStatus.PendingKyc:
newStatus = WithdrawalGroupStatus.SuspendedKyc;
break;
- case WithdrawalGroupStatus.PendingAml:
- newStatus = WithdrawalGroupStatus.SuspendedAml;
- break;
default:
logger.warn(
`Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
@@ -472,7 +613,7 @@ export class WithdrawTransactionContext implements TransactionContext {
);
}
- async abortTransaction(): Promise<void> {
+ async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { withdrawalGroupId } = this;
await this.transition(
{
@@ -491,13 +632,15 @@ export class WithdrawTransactionContext implements TransactionContext {
case WithdrawalGroupStatus.PendingRegisteringBank:
newStatus = WithdrawalGroupStatus.AbortingBank;
break;
- case WithdrawalGroupStatus.SuspendedAml:
case WithdrawalGroupStatus.SuspendedKyc:
case WithdrawalGroupStatus.SuspendedQueryingStatus:
case WithdrawalGroupStatus.SuspendedReady:
- case WithdrawalGroupStatus.PendingAml:
case WithdrawalGroupStatus.PendingKyc:
case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
newStatus = WithdrawalGroupStatus.AbortedExchange;
break;
case WithdrawalGroupStatus.PendingReady:
@@ -522,6 +665,7 @@ export class WithdrawTransactionContext implements TransactionContext {
default:
assertUnreachable(wg.status);
}
+ wg.abortReason = reason;
wg.status = newStatus;
return TransitionResult.transition(wg);
},
@@ -556,9 +700,6 @@ export class WithdrawTransactionContext implements TransactionContext {
case WithdrawalGroupStatus.SuspendedRegisteringBank:
newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
break;
- case WithdrawalGroupStatus.SuspendedAml:
- newStatus = WithdrawalGroupStatus.PendingAml;
- break;
case WithdrawalGroupStatus.SuspendedKyc:
newStatus = WithdrawalGroupStatus.PendingKyc;
break;
@@ -574,7 +715,7 @@ export class WithdrawTransactionContext implements TransactionContext {
);
}
- async failTransaction(): Promise<void> {
+ async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { withdrawalGroupId } = this;
await this.transition(
{
@@ -595,6 +736,7 @@ export class WithdrawTransactionContext implements TransactionContext {
return TransitionResult.stay();
}
wg.status = newStatus;
+ wg.failReason = reason;
return TransitionResult.transition(wg);
},
);
@@ -667,21 +809,11 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.WithdrawCoins,
};
- case WithdrawalGroupStatus.PendingAml:
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.AmlRequired,
- };
case WithdrawalGroupStatus.PendingKyc:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycRequired,
};
- case WithdrawalGroupStatus.SuspendedAml:
- return {
- major: TransactionMajorState.Suspended,
- minor: TransactionMinorState.AmlRequired,
- };
case WithdrawalGroupStatus.SuspendedKyc:
return {
major: TransactionMajorState.Suspended,
@@ -717,6 +849,26 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.CompletedByOtherWallet,
};
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycRequired,
+ };
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
}
}
@@ -772,20 +924,12 @@ export function computeWithdrawalTransactionActions(
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedReady:
return [TransactionAction.Resume, TransactionAction.Abort];
- case WithdrawalGroupStatus.PendingAml:
- return [
- TransactionAction.Retry,
- TransactionAction.Resume,
- TransactionAction.Abort,
- ];
case WithdrawalGroupStatus.PendingKyc:
return [
+ TransactionAction.Suspend,
TransactionAction.Retry,
- TransactionAction.Resume,
TransactionAction.Abort,
];
- case WithdrawalGroupStatus.SuspendedAml:
- return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.FailedAbortingBank:
@@ -796,6 +940,110 @@ export function computeWithdrawalTransactionActions(
return [TransactionAction.Delete];
case WithdrawalGroupStatus.DialogProposed:
return [TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ ];
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ ];
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
+ }
+}
+
+async function processWithdrawalGroupBalanceKyc(
+ ctx: WithdrawTransactionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
+ const amount = withdrawalGroup.effectiveWithdrawalAmount;
+ if (!exchangeBaseUrl) {
+ throw Error(
+ "invalid state (expected withdrawal group to have exchange base URL)",
+ );
+ }
+ if (!amount) {
+ throw Error(
+ "invalid state (expected withdrawal group to have effective withdrawal amount)",
+ );
+ }
+
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ withdrawalGroup.status ===
+ WithdrawalGroupStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
+ });
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ if (wg.status !== WithdrawalGroupStatus.PendingBalanceKyc) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingReady;
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ withdrawalGroup.status === WithdrawalGroupStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ if (wg.status !== WithdrawalGroupStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingBalanceKyc;
+ wg.kycAccessToken = ret.walletKycAccessToken;
+ delete wg.kycPaytoHash;
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
}
}
@@ -826,12 +1074,19 @@ async function processWithdrawalGroupDialogProposed(
);
url.searchParams.set("old_state", "pending");
- url.searchParams.set("long_poll_ms", "30000");
- const resp = await ctx.wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: ctx.wex.cancellationToken,
- });
+ const resp = await ctx.wex.ws.runLongpollQueueing(
+ ctx.wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("long_poll_ms", `${timeoutMs}`);
+
+ return await ctx.wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: ctx.wex.cancellationToken,
+ });
+ },
+ );
// If the bank claims that the withdrawal operation is already
// pending, but we're still in DialogProposed, some other wallet
@@ -899,7 +1154,13 @@ export async function getBankWithdrawalInfo(
);
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(
+ new Error("failed to get bank remote config"),
+ );
+ }
}
const { body: status } = resp;
@@ -1093,13 +1354,10 @@ interface WithdrawalBatchResult {
batchResp: ExchangeWithdrawBatchResponse;
}
-// FIXME: Move to exchange API types
-enum ExchangeAmlStatus {
- Normal = 0,
- Pending = 1,
- Frozen = 2,
-}
-
+/**
+ * Transition a transaction from pending(ready)
+ * into a pending(kyc|aml) state, in case KYC is required.
+ */
async function handleKycRequired(
wex: WalletExecutionContext,
withdrawalGroup: WithdrawalGroupRecord,
@@ -1109,27 +1367,27 @@ async function handleKycRequired(
): Promise<void> {
logger.info("withdrawal requires KYC");
const respJson = await resp.json();
- const uuidResp = codecForWalletKycUuid().decode(respJson);
+ const legiRequiredResp =
+ codecForLegitimizationNeededResponse().decode(respJson);
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
- logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
+ logger.info(`kyc uuid response: ${j2s(legiRequiredResp)}`);
const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
- const userType = "individual";
- const kycInfo: KycPendingInfo = {
- paytoHash: uuidResp.h_payto,
- requirementRow: uuidResp.requirement_row,
- };
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: withdrawalGroup.reservePriv,
+ accountPub: withdrawalGroup.reservePub,
+ });
+ const url = new URL(`kyc-check/${legiRequiredResp.h_payto}`, exchangeUrl);
logger.info(`kyc url ${url.href}`);
+ // We do not longpoll here, as this is the initial request to get information about the KYC.
const kycStatusRes = await wex.http.fetch(url.href, {
method: "GET",
cancellationToken: wex.cancellationToken,
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
});
- let kycUrl: string;
- let amlStatus: ExchangeAmlStatus | undefined;
+ let kycStatus: AccountKycStatus;
if (
kycStatusRes.status === HttpStatusCode.Ok ||
// FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -1139,17 +1397,16 @@ async function handleKycRequired(
logger.warn("kyc requested, but already fulfilled");
return;
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
+ kycStatus = await readSuccessResponseJsonOrThrow(
+ kycStatusRes,
+ codecForAccountKycStatus(),
+ );
logger.info(`kyc status: ${j2s(kycStatus)}`);
- kycUrl = kycStatus.kyc_url;
- } else if (
- kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
- ) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`aml status: ${j2s(kycStatus)}`);
- amlStatus = kycStatus.aml_status;
} else {
- throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ throwUnexpectedRequestError(
+ kycStatusRes,
+ await readTalerErrorResponse(kycStatusRes),
+ );
}
await ctx.transition(
@@ -1174,19 +1431,9 @@ async function handleKycRequired(
if (wg2.status !== WithdrawalGroupStatus.PendingReady) {
return TransitionResult.stay();
}
- wg2.kycPending = {
- paytoHash: uuidResp.h_payto,
- requirementRow: uuidResp.requirement_row,
- };
- wg2.kycUrl = kycUrl;
- wg2.status =
- amlStatus === ExchangeAmlStatus.Normal || amlStatus === undefined
- ? WithdrawalGroupStatus.PendingKyc
- : amlStatus === ExchangeAmlStatus.Pending
- ? WithdrawalGroupStatus.PendingAml
- : amlStatus === ExchangeAmlStatus.Frozen
- ? WithdrawalGroupStatus.SuspendedAml
- : assertUnreachable(amlStatus);
+ wg2.kycPaytoHash = legiRequiredResp.h_payto;
+ wg2.kycAccessToken = kycStatus.access_token;
+ wg2.status = WithdrawalGroupStatus.PendingKyc;
return TransitionResult.transition(wg2);
},
);
@@ -1295,6 +1542,10 @@ async function processPlanchetExchangeBatchRequest(
withdrawalGroup.exchangeBaseUrl,
).href;
+ // if (logger.shouldLogTrace()) {
+ // logger.trace(`batch-withdraw request: ${j2s(batchReq)}`);
+ // }
+
try {
const resp = await wex.http.fetch(reqUrl, {
method: "POST",
@@ -1463,7 +1714,6 @@ async function processPlanchetVerifyAndStoreCoin(
sourceTransactionId: transactionId,
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
ageCommitmentProof: planchet.ageCommitmentProof,
- spendAllocation: undefined,
};
const planchetCoinPub = planchet.coinPub;
@@ -1616,14 +1866,19 @@ async function processQueryReserve(
`reserves/${reservePub}`,
withdrawalGroup.exchangeBaseUrl,
);
- reserveUrl.searchParams.set("timeout_ms", "30000");
- logger.trace(`querying reserve status via ${reserveUrl.href}`);
-
- const resp = await wex.http.fetch(reserveUrl.href, {
- timeout: getReserveRequestTimeout(withdrawalGroup),
- cancellationToken: wex.cancellationToken,
- });
+ const resp = await wex.ws.runLongpollQueueing(
+ wex,
+ reserveUrl.hostname,
+ async (timeoutMs) => {
+ reserveUrl.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.trace(`querying reserve status via ${reserveUrl.href}`);
+ return await wex.http.fetch(reserveUrl.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
logger.trace(`reserve status code: HTTP ${resp.status}`);
@@ -1752,27 +2007,35 @@ async function processWithdrawalGroupPendingKyc(
wex,
withdrawalGroup.withdrawalGroupId,
);
- const userType = "individual";
- const kycInfo = withdrawalGroup.kycPending;
- if (!kycInfo) {
+ const kycPaytoHash = withdrawalGroup.kycPaytoHash;
+ if (!kycPaytoHash) {
throw Error("no kyc info available in pending(kyc)");
}
const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
- url.searchParams.set("timeout_ms", "30000");
- logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
- const kycStatusRes = await wex.http.fetch(url.href, {
- method: "GET",
- cancellationToken: wex.cancellationToken,
+ const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl);
+ const sigResp = await wex.cryptoApi.signWalletKycAuth({
+ accountPriv: withdrawalGroup.reservePriv,
+ accountPub: withdrawalGroup.reservePub,
});
+ const kycStatusRes = await wex.ws.runLongpollQueueing(
+ wex,
+ url.hostname,
+ async (timeoutMs) => {
+ url.searchParams.set("timeout_ms", `${timeoutMs}`);
+ logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
+ return await wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: wex.cancellationToken,
+ headers: {
+ ["Account-Owner-Signature"]: sigResp.sig,
+ },
+ });
+ },
+ );
+
logger.info(`kyc long-polling response status: HTTP ${kycStatusRes.status}`);
if (
kycStatusRes.status === HttpStatusCode.Ok ||
- // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
kycStatusRes.status === HttpStatusCode.NoContent
) {
await ctx.transition({}, async (rec) => {
@@ -1781,8 +2044,8 @@ async function processWithdrawalGroupPendingKyc(
}
switch (rec.status) {
case WithdrawalGroupStatus.PendingKyc: {
- delete rec.kycPending;
- delete rec.kycUrl;
+ delete rec.kycAccessToken;
+ delete rec.kycAccessToken;
rec.status = WithdrawalGroupStatus.PendingReady;
return TransitionResult.transition(rec);
}
@@ -1791,28 +2054,8 @@ async function processWithdrawalGroupPendingKyc(
}
});
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
- const kycUrl = kycStatus.kyc_url;
- if (typeof kycUrl === "string") {
- await ctx.transition({}, async (rec) => {
- if (!rec) {
- return TransitionResult.stay();
- }
- switch (rec.status) {
- case WithdrawalGroupStatus.PendingReady: {
- rec.kycUrl = kycUrl;
- return TransitionResult.transition(rec);
- }
- }
- return TransitionResult.stay();
- });
- }
- } else if (
- kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
- ) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`aml status: ${j2s(kycStatus)}`);
+ logger.info("kyc not done yet, long-poll remains pending");
+ return TaskRunResult.longpollReturnedPending();
} else {
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
}
@@ -1954,14 +2197,6 @@ async function redenominateWithdrawal(
.toString(),
hasDenomWithAgeRestriction:
prevHasDenomWithAgeRestriction || newSel.hasDenomWithAgeRestriction,
- earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.min(
- prevEarliestDepositExpiration,
- AbsoluteTime.fromProtocolTimestamp(
- newSel.earliestDepositExpiration,
- ),
- ),
- ),
};
wg.denomsSel = mergedSel;
if (logger.shouldLogTrace()) {
@@ -2004,6 +2239,33 @@ async function processWithdrawalGroupPendingReady(
return TaskRunResult.finished();
}
+ checkDbInvariant(
+ withdrawalGroup.effectiveWithdrawalAmount != null,
+ "expected withdrawal group to have effective amount",
+ );
+
+ const kycCheckRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ wex,
+ withdrawalGroup.exchangeBaseUrl,
+ withdrawalGroup.effectiveWithdrawalAmount,
+ );
+
+ if (kycCheckRes.result === "violation") {
+ // Do this before we transition so that the exchange is already in the right state.
+ await handleStartExchangeWalletKyc(wex, {
+ amount: kycCheckRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingBalanceKycInit;
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.progress();
+ }
+
const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
.map((x) => x.count)
.reduce((a, b) => a + b);
@@ -2133,11 +2395,6 @@ async function processWithdrawalGroupPendingReady(
throw Error("withdrawal group does not exist anymore");
}
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
- });
-
if (numPlanchetErrors > 0) {
return {
type: TaskRunResultType.Error,
@@ -2158,6 +2415,10 @@ export async function processWithdrawalGroup(
wex: WalletExecutionContext,
withdrawalGroupId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
logger.trace("processing withdrawal group", withdrawalGroupId);
const withdrawalGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["withdrawalGroups"] },
@@ -2179,11 +2440,8 @@ export async function processWithdrawalGroup(
return processQueryReserve(wex, withdrawalGroupId);
case WithdrawalGroupStatus.PendingWaitConfirmBank:
return await processReserveBankStatus(wex, withdrawalGroupId);
- case WithdrawalGroupStatus.PendingAml:
- // FIXME: Handle this case, withdrawal doesn't support AML yet.
- return TaskRunResult.backoff();
case WithdrawalGroupStatus.PendingKyc:
- return processWithdrawalGroupPendingKyc(wex, withdrawalGroup);
+ return await processWithdrawalGroupPendingKyc(wex, withdrawalGroup);
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
return await processWithdrawalGroupPendingReady(wex, withdrawalGroup);
@@ -2191,16 +2449,20 @@ export async function processWithdrawalGroup(
return await processWithdrawalGroupAbortingBank(wex, withdrawalGroup);
case WithdrawalGroupStatus.DialogProposed:
return await processWithdrawalGroupDialogProposed(ctx, withdrawalGroup);
+ case WithdrawalGroupStatus.PendingBalanceKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ return await processWithdrawalGroupBalanceKyc(ctx, withdrawalGroup);
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.FailedAbortingBank:
case WithdrawalGroupStatus.SuspendedAbortingBank:
- case WithdrawalGroupStatus.SuspendedAml:
case WithdrawalGroupStatus.SuspendedKyc:
case WithdrawalGroupStatus.SuspendedQueryingStatus:
case WithdrawalGroupStatus.SuspendedReady:
case WithdrawalGroupStatus.SuspendedRegisteringBank:
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedUserRefused:
@@ -2268,14 +2530,6 @@ export async function getExchangeWithdrawalInfo(
logger.trace("selection done");
- if (selectedDenoms.selectedDenoms.length === 0) {
- throw Error(
- `unable to withdraw from ${exchangeBaseUrl}, can't select denominations for instructed amount (${Amounts.stringify(
- instructedAmount,
- )}`,
- );
- }
-
const exchangeWireAccounts: string[] = [];
for (const account of exchange.wireInfo.accounts) {
@@ -2314,14 +2568,10 @@ export async function getExchangeWithdrawalInfo(
}
const ret: ExchangeWithdrawalDetails = {
- earliestDepositExpiration: selectedDenoms.earliestDepositExpiration,
exchangePaytoUris: paytoUris,
exchangeWireAccounts,
exchangeCreditAccountDetails: withdrawalAccountsList,
- exchangeVersion: exchange.protocolVersionRange || "unknown",
selectedDenoms,
- versionMatch,
- walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
termsOfServiceAccepted: tosAccepted,
withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
withdrawalAmountRaw: Amounts.stringify(instructedAmount),
@@ -2335,25 +2585,10 @@ export async function getExchangeWithdrawalInfo(
return ret;
}
-export interface GetWithdrawalDetailsForUriOpts {
- restrictAge?: number;
- notifyChangeFromPendingTimeoutMs?: number;
-}
-
-/**
- * Get more information about a taler://withdraw URI.
- *
- * As side effects, the bank (via the bank integration API) is queried
- * and the exchange suggested by the bank is ephemerally added
- * to the wallet's list of known exchanges.
- */
-export async function getWithdrawalDetailsForUri(
+async function getWithdrawalDetailsForBankInfo(
wex: WalletExecutionContext,
- talerWithdrawUri: string,
+ info: BankWithdrawDetails,
): Promise<WithdrawUriInfoResponse> {
- logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
- const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri);
- logger.trace(`got bank info`);
if (info.exchange) {
try {
// If the exchange entry doesn't exist yet,
@@ -2379,6 +2614,12 @@ export async function getWithdrawalDetailsForUri(
} else {
const listExchangesResp = await listExchanges(wex);
+ for (const exchange of listExchangesResp.exchanges) {
+ if (exchange.currency !== currency) {
+ continue;
+ }
+ }
+
possibleExchanges = listExchangesResp.exchanges.filter((x) => {
return (
x.currency === currency &&
@@ -2403,6 +2644,23 @@ export async function getWithdrawalDetailsForUri(
};
}
+/**
+ * Get more information about a taler://withdraw URI.
+ *
+ * As side effects, the bank (via the bank integration API) is queried
+ * and the exchange suggested by the bank is ephemerally added
+ * to the wallet's list of known exchanges.
+ */
+export async function getWithdrawalDetailsForUri(
+ wex: WalletExecutionContext,
+ talerWithdrawUri: string,
+): Promise<WithdrawUriInfoResponse> {
+ logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
+ const info = await getBankWithdrawalInfo(wex.http, talerWithdrawUri);
+ logger.trace(`got bank info`);
+ return getWithdrawalDetailsForBankInfo(wex, info);
+}
+
export function augmentPaytoUrisForWithdrawal(
plainPaytoUris: string[],
reservePub: string,
@@ -2416,6 +2674,19 @@ export function augmentPaytoUrisForWithdrawal(
);
}
+export function augmentPaytoUrisForKycTransfer(
+ plainPaytoUris: string[],
+ reservePub: string,
+ tinyAmount: AmountLike,
+): string[] {
+ return plainPaytoUris.map((x) =>
+ addPaytoQueryParams(x, {
+ amount: Amounts.stringify(tinyAmount),
+ message: `Taler KYC ${reservePub}`,
+ }),
+ );
+}
+
/**
* Get payto URIs that can be used to fund a withdrawal operation.
*/
@@ -2546,7 +2817,7 @@ async function registerReserveWithBank(
});
const status = await readSuccessResponseJsonOrThrow(
httpResp,
- codeForBankWithdrawalOperationPostResponse(),
+ codecForBankWithdrawalOperationPostResponse(),
);
await ctx.transition({}, async (r) => {
@@ -2790,7 +3061,7 @@ export async function internalPrepareCreateWithdrawalGroup(
exchangeBaseUrl: string | undefined;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
- reserveKeyPair?: EddsaKeypair;
+ reserveKeyPair?: EddsaKeyPairStrings;
restrictAge?: number;
wgInfo: WgInfo;
},
@@ -2854,7 +3125,6 @@ export async function internalPrepareCreateWithdrawalGroup(
status: args.reserveStatus,
withdrawalGroupId,
restrictAge: args.restrictAge,
- senderWire: undefined,
timestampFinish: undefined,
wgInfo: args.wgInfo,
};
@@ -2986,7 +3256,7 @@ export async function internalCreateWithdrawalGroup(
amount?: AmountJson;
forcedWithdrawalGroupId?: string;
forcedDenomSel?: ForcedDenomSel;
- reserveKeyPair?: EddsaKeypair;
+ reserveKeyPair?: EddsaKeyPairStrings;
restrictAge?: number;
wgInfo: WgInfo;
},
@@ -3007,13 +3277,13 @@ export async function internalCreateWithdrawalGroup(
"reserves",
"exchanges",
"exchangeDetails",
- "transactions",
+ "transactionsMeta",
"operationRetries",
],
},
async (tx) => {
const res = await internalPerformCreateWithdrawalGroup(wex, tx, prep);
- await updateWithdrawalTransaction(ctx, tx);
+ await ctx.updateTransactionMeta(tx);
return res;
},
);
@@ -3039,8 +3309,25 @@ export async function prepareBankIntegratedWithdrawal(
},
);
+ const parsedUri = parseTalerUri(req.talerWithdrawUri);
+ if (parsedUri?.type !== TalerUriAction.Withdraw) {
+ throw TalerError.fromDetail(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {});
+ }
+
+ const externalConfirmation = parsedUri.externalConfirmation;
+
+ logger.info(
+ `creating withdrawal with externalConfirmation=${externalConfirmation}`,
+ );
+
+ const withdrawInfo = await getBankWithdrawalInfo(
+ wex.http,
+ req.talerWithdrawUri,
+ );
+
+ const info = await getWithdrawalDetailsForBankInfo(wex, withdrawInfo);
+
if (existingWithdrawalGroup) {
- const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri);
return {
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
@@ -3049,12 +3336,6 @@ export async function prepareBankIntegratedWithdrawal(
info,
};
}
- const withdrawInfo = await getBankWithdrawalInfo(
- wex.http,
- req.talerWithdrawUri,
- );
-
- const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri);
/**
* Withdrawal group without exchange and amount
@@ -3074,13 +3355,14 @@ export async function prepareBankIntegratedWithdrawal(
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
currency: withdrawInfo.currency,
+ senderWire: withdrawInfo.senderWire,
+ externalConfirmation,
},
},
reserveStatus: WithdrawalGroupStatus.DialogProposed,
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
@@ -3094,7 +3376,7 @@ export async function prepareBankIntegratedWithdrawal(
export async function confirmWithdrawal(
wex: WalletExecutionContext,
req: ConfirmWithdrawalRequest,
-): Promise<void> {
+): Promise<AcceptWithdrawalResponse> {
const parsedTx = parseTransactionIdentifier(req.transactionId);
const selectedExchange = req.exchangeBaseUrl;
const instructedAmount = Amounts.parseOrThrow(req.amount);
@@ -3121,9 +3403,9 @@ export async function confirmWithdrawal(
}
const exchange = await fetchFreshExchange(wex, selectedExchange);
+ requireExchangeTosAcceptedOrThrow(exchange);
const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
- const confirmUrl = withdrawalGroup.wgInfo.bankInfo.confirmUrl;
/**
* The only reason this could be undefined is because it is an old wallet
@@ -3146,6 +3428,10 @@ export async function confirmWithdrawal(
bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency;
}
+ if (exchange.currency !== bankCurrency) {
+ throw Error("currency mismatch between exchange and bank");
+ }
+
const exchangePaytoUri = await getExchangePaytoUri(
wex,
selectedExchange,
@@ -3161,6 +3447,35 @@ export async function confirmWithdrawal(
wex.cancellationToken,
);
+ const senderWire = withdrawalGroup.wgInfo.bankInfo.senderWire;
+
+ if (senderWire) {
+ logger.info(`sender wire is ${senderWire}`);
+ const parsedSenderWire = parsePaytoUri(senderWire);
+ if (!parsedSenderWire) {
+ throw Error("invalid payto URI");
+ }
+ let acceptable = false;
+ for (const acc of withdrawalAccountList) {
+ const parsedExchangeWire = parsePaytoUri(acc.paytoUri);
+ if (!parsedExchangeWire) {
+ continue;
+ }
+ const checkRes = checkAccountRestriction(
+ senderWire,
+ acc.creditRestrictions ?? [],
+ );
+ if (!checkRes.ok) {
+ continue;
+ }
+ acceptable = true;
+ break;
+ }
+ if (!acceptable) {
+ throw Error("bank account not acceptable by the exchange");
+ }
+ }
+
const ctx = new WithdrawTransactionContext(
wex,
withdrawalGroup.withdrawalGroupId,
@@ -3195,22 +3510,14 @@ export async function confirmWithdrawal(
rec.denomsSel = initalDenoms;
rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost;
rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue;
-
- rec.wgInfo = {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- exchangeCreditAccounts: withdrawalAccountList,
- bankInfo: {
- exchangePaytoUri,
- talerWithdrawUri,
- confirmUrl: confirmUrl,
- timestampBankConfirmed: undefined,
- timestampReserveInfoPosted: undefined,
- wireTypes: bankWireTypes,
- currency: bankCurrency,
- },
- };
- pending = true;
+ checkDbInvariant(
+ rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
+ "withdrawal type mismatch",
+ );
+ rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
+ rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
+ pending = true;
return TransitionResult.transition(rec);
}
default: {
@@ -3223,11 +3530,6 @@ export async function confirmWithdrawal(
await wex.taskScheduler.resetTaskRetries(ctx.taskId);
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: ctx.transactionId,
- });
-
const res = await wex.db.runReadWriteTx(
{
storeNames: ["exchanges"],
@@ -3247,8 +3549,23 @@ export async function confirmWithdrawal(
}
if (pending) {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: "waiting for withdrawal operation to be registered with bank",
+ });
await waitWithdrawalRegistered(wex, ctx);
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents:
+ "done waiting for withdrawal operation to be registered with bank",
+ });
}
+
+ return {
+ reservePub: withdrawalGroup.reservePub,
+ transactionId: req.transactionId as TransactionIdStr,
+ confirmTransferUrl: withdrawalGroup.wgInfo.bankInfo.confirmUrl,
+ };
}
/**
@@ -3258,10 +3575,8 @@ export async function confirmWithdrawal(
*
* Thus after this call returns, the withdrawal operation can be confirmed
* with the bank.
- *
- * @deprecated in favor of prepare/accept
*/
-export async function acceptWithdrawalFromUri(
+export async function acceptBankIntegratedWithdrawal(
wex: WalletExecutionContext,
req: {
talerWithdrawUri: string;
@@ -3271,6 +3586,11 @@ export async function acceptWithdrawalFromUri(
amount?: AmountLike;
},
): Promise<AcceptWithdrawalResponse> {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: "at start of acceptBankIntegratedWithdrawal",
+ });
+
const selectedExchange = req.selectedExchange;
logger.info(
`preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
@@ -3280,6 +3600,11 @@ export async function acceptWithdrawalFromUri(
talerWithdrawUri: req.talerWithdrawUri,
});
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: "prepared acceptBankIntegratedWithdrawal",
+ });
+
let amount: AmountString;
if (p.info.amount == null) {
if (req.amount == null) {
@@ -3287,14 +3612,21 @@ export async function acceptWithdrawalFromUri(
"amount required, as withdrawal operation has flexible amount",
);
}
- amount = req.amount as AmountString;
+ amount = Amounts.stringify(req.amount);
} else {
- if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) {
- throw Error(
- "mismatched amount, amount is fixed by bank but client provided different amount",
- );
+ if (req.amount == null) {
+ amount = p.info.amount;
+ } else {
+ if (
+ Amounts.cmp(p.info.amount, req.amount) != 0 &&
+ !p.info.editableAmount
+ ) {
+ throw Error(
+ `mismatched amount, amount is fixed by bank (${p.info.amount}) but client provided different amount (${req.amount})`,
+ );
+ }
+ amount = Amounts.stringify(req.amount);
}
- amount = p.info.amount;
}
logger.info(`confirming withdrawal with tx ${p.transactionId}`);
@@ -3306,6 +3638,11 @@ export async function acceptWithdrawalFromUri(
forcedDenomSel: req.forcedDenomSel,
});
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: "confirmed acceptBankIntegratedWithdrawal",
+ });
+
const newWithdrawralGroup = await wex.db.runReadOnlyTx(
{ storeNames: ["withdrawalGroups"] },
async (tx) => {
@@ -3354,7 +3691,6 @@ async function waitWithdrawalRegistered(
{},
);
case WithdrawalGroupStatus.PendingKyc:
- case WithdrawalGroupStatus.PendingAml:
case WithdrawalGroupStatus.PendingQueryingStatus:
case WithdrawalGroupStatus.PendingReady:
case WithdrawalGroupStatus.Done:
@@ -3386,6 +3722,7 @@ async function waitWithdrawalRegistered(
async function fetchAccount(
wex: WalletExecutionContext,
instructedAmount: AmountJson,
+ scopeInfo: ScopeInfo,
acct: ExchangeWireAccount,
reservePub: string | undefined,
cancellationToken: CancellationToken,
@@ -3436,6 +3773,20 @@ async function fetchAccount(
} else {
paytoUri = acct.payto_uri;
transferAmount = Amounts.stringify(instructedAmount);
+
+ // fetch currency specification from DB
+ const resp = await wex.db.runReadOnlyTx(
+ {
+ storeNames: ["currencyInfo"],
+ },
+ async (tx) => {
+ return WalletDbHelpers.getCurrencyInfo(tx, scopeInfo);
+ },
+ );
+
+ if (resp) {
+ currencySpecification = resp.currencySpec;
+ }
}
paytoUri = addPaytoQueryParams(paytoUri, {
amount: Amounts.stringify(transferAmount),
@@ -3480,6 +3831,7 @@ async function fetchWithdrawalAccountInfo(
const acctInfo = await fetchAccount(
wex,
req.instructedAmount,
+ req.exchange.scopeInfo,
acct,
req.reservePub,
cancellationToken,
@@ -3523,7 +3875,7 @@ export async function createManualWithdrawal(
);
}
- let reserveKeyPair: EddsaKeypair;
+ let reserveKeyPair: EddsaKeyPairStrings;
if (req.forceReservePriv) {
const pubResp = await wex.cryptoApi.eddsaGetPublic({
priv: req.forceReservePriv,
@@ -3663,46 +4015,17 @@ async function internalWaitWithdrawalFinal(
export async function getWithdrawalDetailsForAmount(
wex: WalletExecutionContext,
- cts: CancellationToken.Source,
req: GetWithdrawalDetailsForAmountRequest,
): Promise<WithdrawalDetailsForAmount> {
- const clientCancelKey = req.clientCancellationId
- ? `ccid:getWithdrawalDetailsForAmount:${req.clientCancellationId}`
- : undefined;
- if (clientCancelKey) {
- const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey);
- if (prevCts) {
- wex.oc.observe({
- type: ObservabilityEventType.Message,
- contents: `Cancelling previous key ${clientCancelKey}`,
- });
- prevCts.cancel(`getting details amount`);
- } else {
- wex.oc.observe({
- type: ObservabilityEventType.Message,
- contents: `No previous key ${clientCancelKey}`,
- });
- }
- wex.oc.observe({
- type: ObservabilityEventType.Message,
- contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`,
- });
- wex.ws.clientCancellationMap.set(clientCancelKey, cts);
- }
- try {
- return await internalGetWithdrawalDetailsForAmount(wex, req);
- } finally {
- wex.oc.observe({
- type: ObservabilityEventType.Message,
- contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`,
- });
- if (clientCancelKey && !cts.token.isCancelled) {
- wex.ws.clientCancellationMap.delete(clientCancelKey);
- }
- }
+ return runWithClientCancellation(
+ wex,
+ "getWithdrawalDetailsForAmount",
+ req.clientCancellationId,
+ async () => internalGetWithdrawalDetailsForAmount(wex, req),
+ );
}
-async function internalGetWithdrawalDetailsForAmount(
+export async function internalGetWithdrawalDetailsForAmount(
wex: WalletExecutionContext,
req: GetWithdrawalDetailsForAmountRequest,
): Promise<WithdrawalDetailsForAmount> {
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index fe64396fb..62eb3c824 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 88f152d50..431cfa8a9 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.11.4",
+ "version": "0.13.4",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.11.4"
+ "version_name": "0.13.4"
}
diff --git a/packages/taler-wallet-webextension/manifest-v2.json b/packages/taler-wallet-webextension/manifest-v2.json
index 6f2096b05..459277d3d 100644
--- a/packages/taler-wallet-webextension/manifest-v2.json
+++ b/packages/taler-wallet-webextension/manifest-v2.json
@@ -5,7 +5,7 @@
"browser_specific_settings": {
"gecko": {
"id": "wallet@taler.net",
- "strict_min_version": "57.0"
+ "strict_min_version": "58.0"
}
},
"commands": {
@@ -82,4 +82,4 @@
"page": "static/background.html",
"persistent": true
}
-} \ No newline at end of file
+}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 90679cfdd..623556099 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index fe348f7fb..66d47c180 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -39,7 +39,10 @@ import qrIcon from "./svg/qr_code_24px.inline.svg";
import settingsIcon from "./svg/settings_black_24dp.inline.svg";
import warningIcon from "./svg/warning_24px.inline.svg";
import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ encodeCrockForURI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
/**
* List of pages used by the wallet
@@ -60,10 +63,7 @@ function replaceAll(
): string {
let result = pattern;
for (const v in vars) {
- result = result.replace(
- vars[v],
- !values[v] ? "" : encodeURIComponent(values[v]),
- );
+ result = result.replace(vars[v], !values[v] ? "" : values[v]);
}
return result;
}
@@ -94,23 +94,43 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
return f;
}
+/**
+ * taler action and scope info may contain
+ * character not suitable for URL
+ */
+type CrockEncodedString = string;
+
export const Pages = {
welcome: "/welcome",
balance: "/balance",
- balanceHistory: pageDefinition<{ currency?: string }>(
- "/balance/history/:currency?",
- ),
- searchHistory: pageDefinition<{ currency?: string }>(
- "/search/history/:currency?",
+ balanceHistory: pageDefinition<{ scope?: CrockEncodedString }>(
+ "/balance/history/:scope?",
),
- balanceDeposit: pageDefinition<{ amount: string }>(
- "/balance/deposit/:amount",
+ searchHistory: pageDefinition<{ scope?: CrockEncodedString }>(
+ "/search/history/:scope?",
),
balanceTransaction: pageDefinition<{ tid: string }>(
"/balance/transaction/:tid",
),
- sendCash: pageDefinition<{ amount?: string }>("/destination/send/:amount"),
- receiveCash: pageDefinition<{ amount?: string }>("/destination/get/:amount?"),
+ bankManange: pageDefinition<{ scope: CrockEncodedString }>(
+ "/bank/manage/:scope",
+ ),
+ balanceDeposit: pageDefinition<{
+ scope: CrockEncodedString;
+ }>("/balance/deposit/:scope"),
+ sendCash: pageDefinition<{ scope: CrockEncodedString; amount?: string }>(
+ "/destination/send/:scope/:amount?",
+ ),
+ // if no scope is specified, then exchange selection page will be shown
+ receiveCash: pageDefinition<{ scope?: CrockEncodedString; amount?: string }>(
+ "/destination/get/:scope?/:amount?",
+ ),
+ receiveCashForPurchase: pageDefinition<{ id?: string }>(
+ "/add-for-payment/purchase/:id",
+ ),
+ receiveCashForInvoice: pageDefinition<{ id?: string }>(
+ "/add-for-payment/purchase/:id",
+ ),
dev: "/dev",
exchanges: "/exchanges",
@@ -127,26 +147,39 @@ export const Pages = {
"/settings/exchange/add/:currency?",
),
- defaultCta: pageDefinition<{ uri: string }>("/taler-uri/:uri"),
- cta: pageDefinition<{ action: string }>("/cta/:action"),
+ defaultCta: pageDefinition<{ uri: CrockEncodedString }>("/taler-uri/:uri"),
+ // FIXME: mem leak problems
+ defaultCtaSimple: pageDefinition<{ uri: CrockEncodedString }>(
+ "/taler-uri-simple/:uri",
+ ),
+ cta: pageDefinition<{ action: CrockEncodedString }>("/cta/:action"),
ctaPay: "/cta/pay",
ctaPayTemplate: "/cta/pay/template",
ctaRecovery: "/cta/recovery",
ctaRefund: "/cta/refund",
ctaWithdraw: "/cta/withdraw",
- ctaDeposit: "/cta/deposit",
+ ctaDeposit: pageDefinition<{
+ scope: CrockEncodedString;
+ account: CrockEncodedString;
+ }>("/cta/deposit/:scope/:account"),
ctaExperiment: "/cta/experiment",
ctaAddExchange: "/cta/add/exchange",
- ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
- "/cta/invoice/create/:amount?",
- ),
- ctaTransferCreate: pageDefinition<{ amount?: string }>(
- "/cta/transfer/create/:amount?",
- ),
+ ctaInvoiceCreate: pageDefinition<{
+ scope: CrockEncodedString;
+ }>("/cta/invoice/create/:scope/:amount?"),
+ ctaTransferCreate: pageDefinition<{
+ scope: CrockEncodedString;
+ }>("/cta/transfer/create/:scope/:amount?"),
ctaInvoicePay: "/cta/invoice/pay",
ctaTransferPickup: "/cta/transfer/pickup",
- ctaWithdrawManual: pageDefinition<{ amount?: string }>(
- "/cta/manual-withdraw/:amount?",
+
+ ctaWithdrawManual: pageDefinition<{
+ scope: CrockEncodedString;
+ amount?: string;
+ }>("/cta/manual-withdraw/:scope/:amount?"),
+ paytoQrs: pageDefinition<{ payto: CrockEncodedString }>("/payto/qrs/:payto?"),
+ paytoBanks: pageDefinition<{ payto: CrockEncodedString }>(
+ "/payto/banks/:payto?",
),
};
@@ -178,7 +211,7 @@ export function getPathnameForTalerURI(talerUri: string): string | undefined {
typeof Pages[pageName] === "function"
? (Pages[pageName] as any)()
: Pages[pageName];
- return `${pageString}?talerUri=${encodeURIComponent(talerUri)}`;
+ return `${pageString}?talerUri=${encodeCrockForURI(talerUri)}`;
}
export type PopupNavBarOptions = "balance" | "backup" | "dev";
@@ -195,17 +228,17 @@ export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
const { i18n } = useTranslationContext();
return (
<NavigationHeader>
- <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <a href={`#${Pages.balance}`} class={path === "balance" ? "active" : ""}>
<i18n.Translate>Balance</i18n.Translate>
</a>
<EnabledBySettings name="backup">
- <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <a href={`#${Pages.backup}`} class={path === "backup" ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate>
</a>
</EnabledBySettings>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
{attentionCount > 0 ? (
- <a href={Pages.notifications}>
+ <a href={`#${Pages.notifications}`}>
<SvgIcon
title={i18n.str`Notifications`}
dangerouslySetInnerHTML={{ __html: warningIcon }}
@@ -215,14 +248,14 @@ export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
) : (
<Fragment />
)}
- <a href={Pages.qr}>
+ <a href={`#${Pages.qr}`}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
dangerouslySetInnerHTML={{ __html: qrIcon }}
color="white"
/>
</a>
- <a href={Pages.settings}>
+ <a href={`#${Pages.settings}`}>
<SvgIcon
title={i18n.str`Settings`}
dangerouslySetInnerHTML={{ __html: settingsIcon }}
@@ -250,17 +283,23 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
return (
<NavigationHeaderHolder>
<NavigationHeader>
- <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <a
+ href={`#${Pages.balance}`}
+ class={path === "balance" ? "active" : ""}
+ >
<i18n.Translate>Balance</i18n.Translate>
</a>
<EnabledBySettings name="backup">
- <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <a
+ href={`#${Pages.backup}`}
+ class={path === "backup" ? "active" : ""}
+ >
<i18n.Translate>Backup</i18n.Translate>
</a>
</EnabledBySettings>
{attentionCount > 0 ? (
- <a href={Pages.notifications}>
+ <a href={`#${Pages.notifications}`}>
<i18n.Translate>Notifications</i18n.Translate>
</a>
) : (
@@ -268,7 +307,7 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
)}
<EnabledBySettings name="advancedMode">
- <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
+ <a href={`#${Pages.dev}`} class={path === "dev" ? "active" : ""}>
<i18n.Translate>Dev tools</i18n.Translate>
</a>
</EnabledBySettings>
@@ -276,21 +315,21 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
>
- <a href={Pages.searchHistory({})}>
+ <a href={`#${Pages.searchHistory({})}`}>
<SvgIcon
title={i18n.str`Search transactions`}
dangerouslySetInnerHTML={{ __html: searchIcon }}
color="white"
/>
</a>
- <a href={Pages.qr}>
+ <a href={`#${Pages.qr}`}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
dangerouslySetInnerHTML={{ __html: qrIcon }}
color="white"
/>
</a>
- <a href={Pages.settings}>
+ <a href={`#${Pages.settings}`}>
<SvgIcon
title={i18n.str`Settings`}
dangerouslySetInnerHTML={{ __html: settingsIcon }}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index 6dd577b88..a6ccc10ca 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
+import { Amounts, ScopeInfo, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
import {
TableWithRoundRows as TableWithRoundedRows
@@ -25,7 +25,7 @@ export function BalanceTable({
goToWalletHistory,
}: {
balances: WalletBalance[];
- goToWalletHistory: (currency: string) => void;
+ goToWalletHistory: (currency: ScopeInfo) => void;
}): VNode {
return (
<Fragment>
@@ -36,7 +36,7 @@ export function BalanceTable({
return (
<tr
key={idx}
- onClick={() => goToWalletHistory(av.currency)}
+ onClick={() => goToWalletHistory(entry.scopeInfo)}
style={{ cursor: "pointer" }}
>
<td>{av.currency}</td>
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index 8b6377fc5..65368fd81 100644
--- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -17,19 +17,34 @@
import {
AmountJson,
Amounts,
+ AmountString,
parsePaytoUri,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ PaytoUriUnknown,
segwitMinAmount,
stringifyPaytoUri,
TranslatedString,
WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ encodeCrockForURI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
+import { Button } from "../mui/Button.js";
import { CopiedIcon, CopyIcon } from "../svg/index.js";
import { Amount } from "./Amount.js";
import { ButtonBox, TooltipLeft, WarningBox } from "./styled/index.js";
-import { Button } from "../mui/Button.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useBackendContext } from "../context/backend.js";
+import { QR } from "./QR.js";
+import { Pages } from "../NavigationBar.js";
+import { ShowQRsForPaytoPopup } from "./ShowQRsForPaytoPopup.js";
+import { SafeHandler } from "../mui/handlers.js";
+import { ShowBanksForPaytoPopup } from "./ShowBanksForPaytoPopup.js";
export interface BankDetailsProps {
subject: string;
@@ -59,71 +74,12 @@ export function BankDetailsByPaytoType({
const payto = parsePaytoUri(selectedAccount.paytoUri);
if (!payto) return <Fragment />;
+ // make sure the payto has the right params
payto.params["amount"] = altCurrency
? selectedAccount.transferAmount!
: Amounts.stringify(amount);
payto.params["message"] = subject;
- function Frame({
- title,
- children,
- }: {
- title: TranslatedString;
- children: ComponentChildren;
- }): VNode {
- return (
- <section
- style={{
- textAlign: "left",
- border: "solid 1px black",
- padding: 8,
- borderRadius: 4,
- }}
- >
- <div
- style={{
- display: "flex",
- width: "100%",
- justifyContent: "space-between",
- }}
- >
- <p style={{ marginTop: 0 }}>{title}</p>
- <div></div>
- </div>
-
- {children}
-
- {accounts.length > 1 ? (
- <Fragment>
- {accounts.map((ac, acIdx) => {
- const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
- return (
- <Button
- key={acIdx}
- variant={acIdx === index ? "contained" : "outlined"}
- onClick={async () => {
- setIndex(acIdx);
- }}
- >
- {accountLabel} (
- {ac.currencySpecification?.name ?? amount.currency})
- </Button>
- );
- })}
-
- {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
- onClick={async () => {
- setCurrency(altCurrency)
- }}
- >
- <i18n.Translate>{altCurrency}</i18n.Translate>
- </Button> */}
- </Fragment>
- ) : undefined}
- </section>
- );
- }
-
if (payto.isKnown && payto.targetType === "bitcoin") {
const min = segwitMinAmount(amount.currency);
const addrs = payto.segwitAddrs.map(
@@ -132,7 +88,13 @@ export function BankDetailsByPaytoType({
addrs.unshift(`${payto.targetPath} ${Amounts.stringifyValue(amount)}`);
const copyContent = addrs.join("\n");
return (
- <Frame title={i18n.str`Bitcoin transfer details`}>
+ <Frame
+ title={i18n.str`Bitcoin transfer details`}
+ accounts={accounts}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
<p>
<i18n.Translate>
The exchange need a transaction with 3 output, one output is the
@@ -176,6 +138,47 @@ export function BankDetailsByPaytoType({
);
}
+ return (
+ <Frame
+ title={i18n.str`Bank transfer details`}
+ accounts={accounts}
+ updateIndex={setIndex}
+ currentIndex={index}
+ defaultCurrency={amount.currency}
+ >
+ <IBANAccountInfoTable payto={payto} subject={subject} />
+ </Frame>
+ );
+}
+
+function IBANAccountInfoTable({
+ payto,
+ subject,
+}: {
+ subject: string;
+ payto: PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank;
+}) {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const [showBanks, setShowBanks] = useState(false);
+ const [showQrs, setShowQrs] = useState(false);
+
+ const hook = useAsyncAsHook(async () => {
+ const qrs = await api.wallet.call(WalletApiOperation.GetQrCodesForPayto, {
+ paytoUri: stringifyPaytoUri(payto),
+ });
+ const banks = await api.wallet.call(
+ WalletApiOperation.GetBankingChoicesForPayto,
+ {
+ paytoUri: stringifyPaytoUri(payto),
+ },
+ );
+ return { qrs, banks };
+ }, []);
+
+ const qrCodes = !hook || hook.hasError ? [] : hook.response.qrs.codes;
+ const banksSites = !hook || hook.hasError ? [] : hook.response.banks.choices;
+
const accountPart = !payto.isKnown ? (
<Fragment>
<Row name={i18n.str`Account`} value={payto.targetPath} />
@@ -196,96 +199,147 @@ export function BankDetailsByPaytoType({
const receiver =
payto.params["receiver-name"] || payto.params["receiver"] || undefined;
+
return (
- <Frame title={i18n.str`Bank transfer details`}>
- <table>
- <tbody>
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 1:</i18n.Translate>
- &nbsp;
- <i18n.Translate>
- Copy this code and paste it into the subject/purpose field in
- your banking app or bank website
- </i18n.Translate>
- </td>
- </tr>
- <Row name={i18n.str`Subject`} value={subject} literal />
+ <table>
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in your
+ banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row name={i18n.str`Subject`} value={subject} literal />
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 2:</i18n.Translate>
- &nbsp;
- <i18n.Translate>
- If you don't already have it in your banking favourites list,
- then copy and paste this IBAN and the name into the receiver
- fields in your banking app or website
- </i18n.Translate>
- </td>
- </tr>
- {accountPart}
- {receiver ? (
- <Row name={i18n.str`Receiver name`} value={receiver} />
- ) : undefined}
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ If you don't already have it in your banking favourites list, then
+ copy and paste this IBAN and the name into the receiver fields in
+ your banking app or website
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
+ {receiver ? (
+ <Row name={i18n.str`Receiver name`} value={receiver} />
+ ) : undefined}
- <tr>
- <td colSpan={3}>
- <i18n.Translate>Step 3:</i18n.Translate>
- &nbsp;
- <i18n.Translate>
- Finish the wire transfer setting the amount in your banking app
- or website, then this withdrawal will proceed automatically.
- </i18n.Translate>
- </td>
- </tr>
- <Row
- name={i18n.str`Amount`}
- value={
- <Amount
- value={altCurrency ? selectedAccount.transferAmount! : amount}
- hideCurrency
- />
- }
- />
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Finish the wire transfer setting the amount in your banking app or
+ website, then this withdrawal will proceed automatically.
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row
+ name={i18n.str`Amount`}
+ value={
+ <Amount
+ value={payto.params["amount"] as AmountString}
+ hideCurrency
+ />
+ }
+ />
- <tr>
- <td colSpan={3}>
- <WarningBox style={{ margin: 0 }}>
- <span>
- <i18n.Translate>
- Make sure ALL data is correct, including the subject;
- otherwise, the money will not arrive in this wallet. You can
- use the copy buttons (<CopyIcon />) to prevent typing errors
- or the "payto://" URI below to copy just one value.
- </i18n.Translate>
- </span>
- </WarningBox>
- </td>
- </tr>
+ <tr>
+ <td colSpan={3}>
+ <WarningBox style={{ margin: 0 }}>
+ <span>
+ <i18n.Translate>
+ Make sure ALL data is correct, including the subject;
+ otherwise, the money will not arrive in this wallet. You can
+ use the copy buttons (<CopyIcon />) to prevent typing errors
+ or the "payto://" URI below to copy just one value.
+ </i18n.Translate>
+ </span>
+ </WarningBox>
+ </td>
+ </tr>
- <tr>
- <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
- <i18n.Translate>
- Alternative if your bank already supports PayTo URI, you can use
- this{" "}
- <a
- target="_bank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- PayTo URI
- </a>{" "}
- link instead
- </i18n.Translate>
- </td>
- <td>
- <CopyButton getContent={() => stringifyPaytoUri(payto)} />
- </td>
- </tr>
- </tbody>
- </table>
- </Frame>
+ <tr>
+ <td colSpan={3} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can use
+ this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
+ </td>
+ <td>
+ <CopyButton getContent={() => stringifyPaytoUri(payto)} />
+ </td>
+ </tr>
+
+ {banksSites.length < 1 ? undefined : (
+ <Fragment>
+ <div>
+ <a
+ href="#"
+ onClick={(e) => {
+ setShowBanks(true);
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>
+ Continue with banking app or website
+ </i18n.Translate>
+ </a>
+ </div>
+
+ {showBanks ? (
+ <ShowBanksForPaytoPopup
+ banks={banksSites}
+ onClose={{
+ onClick: (async () =>
+ setShowBanks(false)) as SafeHandler<void>,
+ }}
+ />
+ ) : undefined}
+ </Fragment>
+ )}
+
+ {qrCodes.length < 1 ? undefined : (
+ <Fragment>
+ <div>
+ <a
+ href="#"
+ onClick={(e) => {
+ setShowQrs(true);
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>Show QR code</i18n.Translate>
+ </a>
+ </div>
+ {showQrs ? (
+ <ShowQRsForPaytoPopup
+ qrs={qrCodes}
+ onClose={{
+ onClick: (async () => setShowQrs(false)) as SafeHandler<void>,
+ }}
+ />
+ ) : undefined}
+ </Fragment>
+ )}
+ </tbody>
+ </table>
);
}
@@ -358,3 +412,71 @@ function Row({
</tr>
);
}
+
+function Frame({
+ title,
+ children,
+ accounts,
+ defaultCurrency,
+ currentIndex,
+ updateIndex,
+}: {
+ title: TranslatedString;
+ children: ComponentChildren;
+ currentIndex: number;
+ updateIndex: (idx: number) => void;
+ defaultCurrency: string;
+ accounts: WithdrawalExchangeAccountDetails[];
+}): VNode {
+ return (
+ <section
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ width: "100%",
+ justifyContent: "space-between",
+ }}
+ >
+ <p style={{ marginTop: 0 }}>{title}</p>
+ <div></div>
+ </div>
+
+ {children}
+
+ {accounts.length > 1 ? (
+ <Fragment>
+ {accounts.map((ac, acIdx) => {
+ const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ return (
+ <Button
+ key={acIdx}
+ variant={acIdx === currentIndex ? "contained" : "outlined"}
+ onClick={async () => {
+ updateIndex(acIdx);
+ }}
+ >
+ {accountLabel} (
+ {ac.currencySpecification?.name ?? defaultCurrency})
+ </Button>
+ );
+ })}
+
+ {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
+ onClick={async () => {
+ setCurrency(altCurrency)
+ }}
+ >
+ <i18n.Translate>{altCurrency}</i18n.Translate>
+ </Button> */}
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
index b1ed3b02c..38d1b6b6b 100644
--- a/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
+++ b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
@@ -22,6 +22,8 @@ import {
} from "../context/alert.js";
import { Alert } from "../mui/Alert.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ButtonHandler } from "../mui/handlers.js";
+import { Button } from "../mui/Button.js";
/**
*
@@ -99,13 +101,23 @@ function AlertContext({
export function ErrorAlertView({
error,
+ retry,
onClose,
}: {
error: AlertNotification;
+ retry?: ButtonHandler;
onClose?: () => Promise<void>;
}): VNode {
+ const { i18n } = useTranslationContext();
return (
<Wrapper>
+ {!retry ? undefined : (
+ <section>
+ <Button variant="contained" color="success" onClick={retry.onClick}>
+ <i18n.Translate>Retry operation</i18n.Translate>
+ </Button>
+ </section>
+ )}
<AlertView alert={error} onClose={onClose} />
</Wrapper>
);
diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx
index f8c0f1651..9023761bf 100644
--- a/packages/taler-wallet-webextension/src/components/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -52,8 +52,7 @@ const Body = styled.div`
export function Modal({ title, children, onClose }: Props): VNode {
return (
- <div style={{ top: 0, width: "100%", height: "100%" }}>
-
+ <div style={{ top: 0, left: 0, position: "fixed", width: "100%", height: "100%" }}>
<FullSize onClick={onClose?.onClick}>
<div
onClick={(e) => e.stopPropagation()}
diff --git a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
index 7d3cf3f57..3f22e4849 100644
--- a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
+++ b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
@@ -20,10 +20,10 @@ import { Button } from "../mui/Button.js";
import arrowDown from "../svg/chevron-down.inline.svg";
import { ParagraphClickable } from "./styled/index.js";
-export interface Props {
- label: (s: string) => TranslatedString;
- actions: string[];
- onClick: (s: string) => Promise<void>;
+export interface Props<T> {
+ label: (s: T) => TranslatedString;
+ actions: T[];
+ onClick: (s: T) => Promise<void>;
}
/**
@@ -37,19 +37,19 @@ export interface Props {
*
* @returns
*/
-export function MultiActionButton({
+export function MultiActionButton<T>({
label,
actions,
onClick: doClick,
-}: Props): VNode {
- const defaultAction = actions.length > 0 ? actions[0] : "";
+}: Props<T>): VNode {
+ const defaultAction = actions.length > 0 ? actions[0] : "" as T;
const [opened, setOpened] = useState(false);
- const [selected, setSelected] = useState<string>(defaultAction);
+ const [selected, setSelected] = useState<T>(defaultAction);
const canChange = actions.length > 1;
const options = canChange ? actions.filter((a) => a !== selected) : [];
- function select(m: string): void {
+ function select(m: T): void {
setSelected(m);
setOpened(false);
}
diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx
index 6eb72a266..a879da840 100644
--- a/packages/taler-wallet-webextension/src/components/SelectList.tsx
+++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx
@@ -47,7 +47,7 @@ export function SelectList({
<Fragment>
<label
htmlFor={`text-${name}`}
- style={{ marginLeft: "0.5em", fontWeight: "bold" }}
+ style={{ marginLeft: "0.2rem", fontWeight: "bold" }}
>
{" "}
{label}
diff --git a/packages/taler-wallet-webextension/src/components/ShowBanksForPaytoPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowBanksForPaytoPopup.tsx
new file mode 100644
index 000000000..268dcc1b3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ShowBanksForPaytoPopup.tsx
@@ -0,0 +1,61 @@
+/*
+ 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 {
+ BankingChoiceSpec
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { ButtonHandler } from "../mui/handlers.js";
+import { Modal } from "./Modal.js";
+
+const BanksTable = styled.table`
+ width: 100%;
+ border-spacing: 0px;
+ & > tr > td {
+ padding: 5px;
+ }
+ & > tr > td:nth-child(2n) {
+ text-align: right;
+ overflow-wrap: anywhere;
+ }
+ & > tr:nth-child(2n) {
+ background: #ebebeb;
+ }
+`;
+
+interface Props { banks: BankingChoiceSpec[], onClose: ButtonHandler };
+
+export function ShowBanksForPaytoPopup({ banks, onClose }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Modal title="Supported banks" onClose={onClose}>
+ <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
+ <BanksTable>
+ {banks.map((b, idx) => {
+
+ return <tr key={idx}>
+ <td>
+ <a href={b.uri}>{b.label}</a>
+ </td>
+ </tr>
+ })}
+ </BanksTable>
+ </div>
+ </Modal>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/ShowQRsForPaytoPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowQRsForPaytoPopup.tsx
new file mode 100644
index 000000000..a1d1d0269
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ShowQRsForPaytoPopup.tsx
@@ -0,0 +1,94 @@
+/*
+ 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 {
+ QrCodeSpec
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { ButtonHandler } from "../mui/handlers.js";
+import { Modal } from "./Modal.js";
+import { QR } from "./QR.js";
+import { useState } from "preact/hooks";
+
+const QRsTable = styled.table`
+ width: 100%;
+ & > tr > td {
+ padding: 5px;
+ }
+ & > tr > td {
+ border-spacing: 0px;
+ border-radius: 4px;
+ border: 1px black solid;
+ }
+ & > tr > td:nth-child(2n) {
+ text-align: right;
+ overflow-wrap: anywhere;
+ }
+`;
+
+const AccordionCss = styled.div`
+& > .accordion {
+ color: #444;
+ cursor: pointer;
+ padding: 8px;
+ font-size: large;
+ width: 100%;
+ text-align: left;
+ border: none;
+ outline: none;
+ transition: 0.4s;
+}
+
+& > .panel {
+ padding: 0 18px;
+ background-color: white;
+ display: none;
+ overflow: hidden;
+}`
+
+interface Props { qrs: QrCodeSpec[], onClose: ButtonHandler };
+
+function Accordion({ section, content }: { section: string, content: string }): VNode {
+ const [opened, setOpened] = useState(false)
+ return <AccordionCss>
+ <button class={opened ? "accordion active" : "accordion"} onClick={() => { setOpened(!opened) }}>{section}</button>
+ <div class="panel" style={{ display: opened ? "block" : "none" }}>
+ <QR text={content} />
+ </div>
+ </AccordionCss>
+}
+
+export function ShowQRsForPaytoPopup({ qrs, onClose }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Modal title="Qrs" onClose={onClose}>
+ <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
+ <QRsTable>
+ {qrs.map((q, idx) => {
+
+ return <tr key={idx}>
+ <td>
+ <Accordion section={q.type} content={q.qrContent} />
+ </td>
+ </tr>
+ })}
+ </QRsTable>
+ </div>
+ </Modal>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
index 1585e3992..7ef5b95a1 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
@@ -17,7 +17,11 @@
import { ComponentChildren } from "preact";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
-import { SelectFieldHandler, ToggleHandler } from "../../mui/handlers.js";
+import {
+ ButtonHandler,
+ SelectFieldHandler,
+ ToggleHandler,
+} from "../../mui/handlers.js";
import { StateViewMap, compose } from "../../utils/index.js";
import { ErrorAlertView } from "../CurrentAlerts.js";
import { useComponentState } from "./state.js";
@@ -61,6 +65,7 @@ export namespace State {
status: "show-content";
termsAccepted: ToggleHandler;
showingTermsOfService?: ToggleHandler;
+ skipTos: ButtonHandler;
tosLang: SelectFieldHandler;
tosFormat: SelectFieldHandler;
}
@@ -68,7 +73,7 @@ export namespace State {
status: "show-buttons-accepted";
termsAccepted: ToggleHandler;
showingTermsOfService: ToggleHandler;
- children: ComponentChildren,
+ children: ComponentChildren;
}
export interface ShowButtonsNotAccepted extends BaseInfo {
status: "show-buttons-not-accepted";
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
index 76524f0f4..96d14dadf 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
@@ -25,22 +25,28 @@ import { buildTermsOfServiceState } from "./utils.js";
const supportedFormats = {
"text/html": "HTML",
- "text/xml" : "XML",
- "text/markdown" : "Markdown",
- "text/plain" : "Plain text",
- "text/pdf" : "PDF",
-}
-
-export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, children }: Props): State {
+ "text/xml": "XML",
+ "text/markdown": "Markdown",
+ "text/plain": "Plain text",
+ "text/pdf": "PDF",
+};
+
+export function useComponentState({
+ showEvenIfaccepted,
+ exchangeUrl,
+ readOnly,
+ children,
+}: Props): State {
const api = useBackendContext();
const [showContent, setShowContent] = useState<boolean>(!!readOnly);
const { i18n, lang } = useTranslationContext();
- const [tosLang, setTosLang] = useState<string>()
+ const [forceAccepted, setForceAccepted] = useState<boolean>();
+ const [tosLang, setTosLang] = useState<string>();
const { pushAlertOnError } = useAlertContext();
- const [format, setFormat] = useState("text/html")
+ const [format, setFormat] = useState("text/html");
- const acceptedLang = tosLang ?? lang
+ const acceptedLang = tosLang ?? lang;
/**
* For the exchange selected, bring the status of the terms of service
*/
@@ -54,10 +60,13 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
},
);
- const supportedLangs = exchangeTos.tosAvailableLanguages.reduce((prev, cur) => {
- prev[cur] = cur
- return prev;
- }, {} as Record<string, string>)
+ const supportedLangs = exchangeTos.tosAvailableLanguages.reduce(
+ (prev, cur) => {
+ prev[cur] = cur;
+ return prev;
+ },
+ {} as Record<string, string>,
+ );
const state = buildTermsOfServiceState(exchangeTos);
@@ -92,30 +101,35 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
} else {
// mark as not accepted
}
- terms?.retry()
+ terms?.retry();
}
- const accepted = state.status === "accepted";
+ const accepted = state.status === "accepted" || forceAccepted;
const base = {
error: undefined,
showingTermsOfService: {
value: showContent && (!accepted || showEvenIfaccepted),
button: {
- onClick: accepted && !showEvenIfaccepted ? undefined : pushAlertOnError(async () => {
- setShowContent(!showContent);
- }),
+ onClick:
+ accepted && !showEvenIfaccepted
+ ? undefined
+ : pushAlertOnError(async () => {
+ setShowContent(!showContent);
+ }),
},
},
terms: state,
termsAccepted: {
value: accepted,
button: {
- onClick: readOnly ? undefined : pushAlertOnError(async () => {
- const newValue = !accepted; //toggle
- await onUpdate(newValue);
- setShowContent(false);
- }),
+ onClick: readOnly
+ ? undefined
+ : pushAlertOnError(async () => {
+ const newValue = !accepted; //toggle
+ await onUpdate(newValue);
+ setShowContent(false);
+ }),
},
},
};
@@ -135,20 +149,25 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
terms: state,
showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
termsAccepted: base.termsAccepted,
+ skipTos: {
+ onClick: pushAlertOnError(async () => {
+ setForceAccepted(true);
+ }),
+ },
tosFormat: {
onChange: pushAlertOnError(async (s) => {
- setFormat(s)
+ setFormat(s);
}),
list: supportedFormats,
- value: format ?? ""
+ value: format ?? "",
},
tosLang: {
onChange: pushAlertOnError(async (s) => {
- setTosLang(s)
+ setTosLang(s);
}),
list: supportedLangs,
- value: tosLang ?? lang
- }
+ value: tosLang ?? lang,
+ },
};
}
//showing buttons
@@ -156,5 +175,4 @@ export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, c
status: "show-buttons-not-accepted",
...base,
};
-
}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
index 40cfba3bc..8d9d3a499 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
@@ -23,7 +23,7 @@ import {
Input,
LinkSuccess,
TermsOfServiceStyle,
- WarningBox
+ WarningBox,
} from "../../components/styled/index.js";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
@@ -50,7 +50,9 @@ export function ShowButtonsAcceptedTosView({
</LinkSuccess>
</section>
{termsAccepted.button.onClick !== undefined && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <section
+ style={{ justifyContent: "space-around", display: "flex" }}
+ >
<CheckboxOutlined
name="terms"
enabled={termsAccepted.value}
@@ -75,36 +77,9 @@ export function ShowButtonsNonAcceptedTosView({
terms,
}: State.ShowButtonsNotAccepted): VNode {
const { i18n } = useTranslationContext();
- // const ableToReviewTermsOfService =
- // showingTermsOfService.button.onClick !== undefined;
-
- // if (!ableToReviewTermsOfService) {
- // return (
- // <Fragment>
- // {terms.status === ExchangeTosStatus.Pending && (
- // <section style={{ justifyContent: "space-around", display: "flex" }}>
- // <WarningText>
- // <i18n.Translate>
- // Exchange doesn&apos;t have terms of service
- // </i18n.Translate>
- // </WarningText>
- // </section>
- // )}
- // </Fragment>
- // );
- // }
return (
<Fragment>
- {/* {terms.status === ExchangeTosStatus.NotFound && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
- <WarningText>
- <i18n.Translate>
- Exchange doesn&apos;t have terms of service
- </i18n.Translate>
- </WarningText>
- </section>
- )} */}
<section style={{ justifyContent: "space-around", display: "flex" }}>
<Button
variant="contained"
@@ -124,11 +99,35 @@ export function ShowTosContentView({
terms,
tosLang,
tosFormat,
+ skipTos,
}: State.ShowContent): VNode {
const { i18n } = useTranslationContext();
- const ableToReviewTermsOfService =
- termsAccepted.button.onClick !== undefined;
+ const ableToReviewTermsOfService = termsAccepted.button.onClick !== undefined;
+
+ if (terms.status === ExchangeTosStatus.MissingTos) {
+ return (
+ <section>
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <WarningBox>
+ <i18n.Translate>
+ The exchange doesn't have a terms of service.
+ </i18n.Translate>
+ </WarningBox>
+ </section>
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <Button
+ variant="contained"
+ color="success"
+ disabled={!skipTos.onClick}
+ onClick={skipTos.onClick}
+ >
+ <i18n.Translate>Skip it for now.</i18n.Translate>
+ </Button>
+ </section>
+ </section>
+ );
+ }
return (
<section>
<Input style={{ display: "flex", justifyContent: "end" }}>
@@ -160,7 +159,18 @@ export function ShowTosContentView({
</section>
)}
{terms.content && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <section
+ style={{
+ justifyContent: "space-around",
+ display: "flex",
+ position: "relative",
+ resize: "vertical",
+ overflow: "hidden",
+ marginTop: "4px",
+ minHeight: "120px",
+ height: "240px",
+ }}
+ >
{terms.content.type === "xml" &&
(!terms.content.document ? (
<WarningBox>
@@ -187,7 +197,18 @@ export function ShowTosContentView({
</div>
))}
{terms.content.type === "html" && (
- <iframe style={{ width: "100%" }} srcDoc={terms.content.html} />
+ <iframe
+ style={{
+ width: "100%",
+ height: "100%",
+ border: "2px solid #0003",
+ borderRadius: "4px",
+ boxSizing: "border-box",
+ }}
+ src={`data:text/html;utf-8,${encodeURIComponent(
+ terms.content.html,
+ )}`}
+ />
)}
{terms.content.type === "pdf" && (
<a href={terms.content.location.toString()} download="tos.pdf">
@@ -206,20 +227,21 @@ export function ShowTosContentView({
</LinkSuccess>
</section>
)}
- {termsAccepted.button.onClick && terms.status !== ExchangeTosStatus.Accepted && (
- <section style={{ justifyContent: "space-around", display: "flex" }}>
- <CheckboxOutlined
- name="terms"
- enabled={termsAccepted.value}
- label={
- <i18n.Translate>
- I accept the exchange terms of service
- </i18n.Translate>
- }
- onToggle={termsAccepted.button.onClick}
- />
- </section>
- )}
+ {termsAccepted.button.onClick &&
+ terms.status !== ExchangeTosStatus.Accepted && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <CheckboxOutlined
+ name="terms"
+ enabled={termsAccepted.value}
+ label={
+ <i18n.Translate>
+ I accept the exchange terms of service
+ </i18n.Translate>
+ }
+ onToggle={termsAccepted.button.onClick}
+ />
+ </section>
+ )}
</section>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
index f29d0b0f7..d8d25f90c 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -39,6 +39,7 @@ import { WxApiType } from "../wxApi.js";
import { WalletActivityTrack } from "../wxBackend.js";
import { Modal } from "./Modal.js";
import { Time } from "./Time.js";
+import { Checkbox } from "./Checkbox.js";
const OPEN_ACTIVITY_HEIGHT_PX = 250;
const CLOSE_ACTIVITY_HEIGHT_PX = 40;
@@ -179,7 +180,7 @@ function ShowBalanceChange({ events }: MoreInfoPRops): VNode {
<dd>
<a
title={not.hintTransactionId}
- href={Pages.balanceTransaction({ tid: not.hintTransactionId })}
+ href={`#${Pages.balanceTransaction({ tid: not.hintTransactionId })}`}
>
{not.hintTransactionId.substring(0, 10)}
</a>
@@ -212,7 +213,10 @@ function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
<dd>{error.hint ?? "--"}</dd>
<dt>Time</dt>
<dd>
- <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss.SSS"
+ />
</dd>
</dl>
<pre
@@ -253,7 +257,7 @@ function ShowTransactionStateTransition({
<dd>
<a
title={not.transactionId}
- href={Pages.balanceTransaction({ tid: not.transactionId })}
+ href={`#${Pages.balanceTransaction({ tid: not.transactionId })}`}
>
{not.transactionId.substring(0, 10)}
</a>
@@ -360,34 +364,44 @@ function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
const title = (function () {
switch (not.event.type) {
- case ObservabilityEventType.HttpFetchFinishError:
- case ObservabilityEventType.HttpFetchFinishSuccess:
case ObservabilityEventType.HttpFetchStart:
return "HTTP Request";
- case ObservabilityEventType.DbQueryFinishSuccess:
- case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess:
+ return "HTTP Request (o)";
+ case ObservabilityEventType.HttpFetchFinishError:
+ return "HTTP Request (x)";
case ObservabilityEventType.DbQueryStart:
return "Database";
- case ObservabilityEventType.RequestFinishSuccess:
- case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ return "Database (o)";
+ case ObservabilityEventType.DbQueryFinishError:
+ return "Database (x)";
case ObservabilityEventType.RequestStart:
return "Wallet";
- case ObservabilityEventType.CryptoFinishSuccess:
- case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.RequestFinishSuccess:
+ return "Wallet (o)";
+ case ObservabilityEventType.RequestFinishError:
+ return "Wallet (x)";
case ObservabilityEventType.CryptoStart:
return "Crypto";
+ case ObservabilityEventType.CryptoFinishSuccess:
+ return "Crypto (o)";
+ case ObservabilityEventType.CryptoFinishError:
+ return "Crypto (x)";
case ObservabilityEventType.TaskStart:
- return "Task start";
+ return "Task";
case ObservabilityEventType.TaskStop:
- return "Task stop";
+ return "Task (s)";
case ObservabilityEventType.TaskReset:
- return "Task reset";
+ return "Task (r)";
case ObservabilityEventType.ShepherdTaskResult:
return "Schedule";
case ObservabilityEventType.DeclareTaskDependency:
return "Task dependency";
case ObservabilityEventType.Message:
return "Message";
+ case ObservabilityEventType.DeclareConcernsTransaction:
+ return "DeclareConcernsTransaction";
}
})();
@@ -401,12 +415,11 @@ function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
);
});
return (
- <table>
+ <table style={{ width: "100%" }}>
<thead>
<td>Event</td>
<td>Info</td>
- <td>Start</td>
- <td>End</td>
+ <td>When</td>
</thead>
<tbody>{asd}</tbody>
</table>
@@ -417,11 +430,9 @@ function ShowObervavilityDetails({
title,
notif,
onClick,
- prev,
}: {
title: string;
notif: ObservaNotifWithTime;
- prev?: ObservaNotifWithTime;
onClick: (content: VNode) => void;
}): VNode {
switch (notif.event.type) {
@@ -443,7 +454,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -454,21 +465,21 @@ function ShowObervavilityDetails({
</td>
<td>
{notif.event.url}{" "}
- {prev?.event.type ===
+ {notif?.event.type ===
ObservabilityEventType.HttpFetchFinishSuccess ? (
- `(${prev.event.status})`
- ) : prev?.event.type ===
+ `(${notif.event.status})`
+ ) : notif?.event.type ===
ObservabilityEventType.HttpFetchFinishError ? (
<a
href="#"
onClick={(e) => {
e.preventDefault();
if (
- prev.event.type !==
+ notif.event.type !==
ObservabilityEventType.HttpFetchFinishError
)
return;
- const error = prev.event.error;
+ const error = notif.event.error;
onClick(
<Fragment>
<dl>
@@ -482,7 +493,7 @@ function ShowObervavilityDetails({
<dd>
<Time
timestamp={error.when}
- format="yyyy/MM/dd HH:mm:ss"
+ format="yyyy/MM/dd HH:mm:ss.SSS"
/>
</dd>
</dl>
@@ -504,11 +515,7 @@ function ShowObervavilityDetails({
</td>
<td>
{" "}
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- {" "}
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
@@ -531,7 +538,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -544,10 +551,7 @@ function ShowObervavilityDetails({
{notif.event.location} {notif.event.name}
</td>
<td>
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
@@ -572,7 +576,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -583,10 +587,7 @@ function ShowObervavilityDetails({
</td>
<td>{notif.event.taskId}</td>
<td>
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
@@ -607,7 +608,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -618,10 +619,7 @@ function ShowObervavilityDetails({
</td>
<td>{notif.event.resultType}</td>
<td>
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
@@ -644,7 +642,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -655,10 +653,7 @@ function ShowObervavilityDetails({
</td>
<td>{notif.event.operation}</td>
<td>
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
@@ -681,7 +676,7 @@ function ShowObervavilityDetails({
wordBreak: "break-word",
}}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ {JSON.stringify({ event: notif }, undefined, 2)}
</pre>
</Fragment>,
);
@@ -692,27 +687,30 @@ function ShowObervavilityDetails({
</td>
<td>{notif.event.type}</td>
<td>
- <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
- </td>
- <td>
- <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</td>
</tr>
);
}
+ case ObservabilityEventType.DeclareConcernsTransaction:
case ObservabilityEventType.Message:
// FIXME
return <></>;
}
}
+function createTabId(tab: chrome.tabs.Tab | undefined) {
+ return !tab ? "popup" : `${tab.windowId}:${tab.id}`;
+}
+
function refresh(
api: WxApiType,
onUpdate: (list: WalletActivityTrack[]) => void,
filter: string,
+ fromView?: chrome.tabs.Tab,
) {
api.background
- .call("getNotifications", { filter })
+ .call("getNotifications", { filter, operationsFrom: createTabId(fromView) })
.then((notif) => {
onUpdate(notif);
})
@@ -721,6 +719,20 @@ function refresh(
});
}
+let currentTab: chrome.tabs.Tab | undefined;
+// Allow running outside the extension for testing
+// tslint:disable-next-line:no-string-literal
+if (typeof chrome !== "undefined" && typeof chrome.tabs !== "undefined") {
+ const p = chrome.tabs.getCurrent();
+ // this may be called outside the render phase (in the background)
+ // when this happen currentTab is not needed but also undefined
+ if (p) {
+ p.then((d) => {
+ currentTab = d;
+ });
+ }
+}
+
export function ObservabilityEventsTable(): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
@@ -728,11 +740,17 @@ export function ObservabilityEventsTable(): VNode {
const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]);
const [showDetails, setShowDetails] = useState<VNode>();
const [filter, onChangeFilter] = useState("");
+ const [onlyThisScreen, setOnlyThisScreen] = useState(true);
useEffect(() => {
let lastTimeout: ReturnType<typeof setTimeout>;
function periodicRefresh() {
- refresh(api, setNotifications, filter);
+ refresh(
+ api,
+ setNotifications,
+ filter,
+ onlyThisScreen ? currentTab : undefined,
+ );
lastTimeout = setTimeout(() => {
periodicRefresh();
@@ -743,7 +761,7 @@ export function ObservabilityEventsTable(): VNode {
};
}
return periodicRefresh();
- }, [filter]);
+ }, [filter, onlyThisScreen]);
return (
<div>
@@ -754,6 +772,12 @@ export function ObservabilityEventsTable(): VNode {
value={filter}
onChange={onChangeFilter}
/>
+ <Checkbox
+ label={i18n.str`All events`}
+ name="terms"
+ onToggle={async () => setOnlyThisScreen((v) => !v)}
+ enabled={!onlyThisScreen}
+ />
<div
style={{
padding: 4,
@@ -763,7 +787,12 @@ export function ObservabilityEventsTable(): VNode {
}}
onClick={() => {
api.background.call("clearNotifications", undefined).then(() => {
- refresh(api, setNotifications, filter);
+ refresh(
+ api,
+ setNotifications,
+ filter,
+ onlyThisScreen ? currentTab : undefined,
+ );
});
}}
>
@@ -784,7 +813,7 @@ export function ObservabilityEventsTable(): VNode {
)}
{notifications.map((not) => {
return (
- <details key={not.id}>
+ <details key={not.groupId}>
<summary>
<div
style={{
@@ -829,10 +858,13 @@ export function ObservabilityEventsTable(): VNode {
})()}
</div>
<div style={{ padding: 4 }}>
- <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" />
+ <Time
+ timestamp={not.start}
+ format="yyyy/MM/dd HH:mm:ss.SSS"
+ />
</div>
<div style={{ padding: 4 }}>
- <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss.SSS" />
</div>
</div>
</summary>
@@ -936,7 +968,7 @@ function ErroDetailModal({
<dd>{error.hint ?? "--"}</dd>
<dt>Time</dt>
<dd>
- <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss.SSS" />
</dd>
</dl>
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
@@ -1009,11 +1041,14 @@ export function ActiveTasksTable(): VNode {
<td>
<Time
timestamp={task.firstTry}
- format="yyyy/MM/dd HH:mm:ss"
+ format="yyyy/MM/dd HH:mm:ss.SSS"
/>
</td>
<td>
- <Time timestamp={task.nextTry} format="yyyy/MM/dd HH:mm:ss" />
+ <Time
+ timestamp={task.nextTry}
+ format="yyyy/MM/dd HH:mm:ss.SSS"
+ />
</td>
<td>
{!task.lastError?.code ? (
@@ -1034,7 +1069,9 @@ export function ActiveTasksTable(): VNode {
{task.transaction ? (
<a
title={task.transaction}
- href={Pages.balanceTransaction({ tid: task.transaction })}
+ href={`#${Pages.balanceTransaction({
+ tid: task.transaction,
+ })}`}
>
{task.transaction.substring(0, 10)}
</a>
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 739b71064..329d1b6e4 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -273,8 +273,15 @@ const Tooltip = styled.div<{ content: string }>`
position: absolute;
z-index: 1000001;
padding: 0.5em 0.75em;
- font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI",
- Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ font:
+ normal normal 11px/1.5 -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Helvetica,
+ Arial,
+ sans-serif,
+ "Apple Color Emoji",
+ "Segoe UI Emoji";
-webkit-font-smoothing: subpixel-antialiased;
color: white;
text-align: center;
@@ -539,7 +546,7 @@ export const LinkPrimary = styled(Link)`
color: black;
`;
-export const ButtonPrimary = styled(ButtonVariant) <{ small?: boolean }>`
+export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
font-size: ${({ small }: any) => (small ? "small" : "inherit")};
background-color: #0042b2;
border-color: #0042b2;
@@ -690,13 +697,13 @@ export const SmallBoldText = styled.div`
font-weight: bold;
`;
-export const AgeSign = styled.div<{size:number}>`
+export const AgeSign = styled.div<{ size: number }>`
display: inline-block;
border: red solid 1px;
border-radius: 100%;
- width: ${({ size }: {size:number}) => (`${size}px`)};
- height: ${({ size }: {size:number}) => (`${size}px`)};
- line-height: ${({ size }: {size:number}) => (`${size}px`)};
+ width: ${({ size }: { size: number }) => `${size}px`};
+ height: ${({ size }: { size: number }) => `${size}px`};
+ line-height: ${({ size }: { size: number }) => `${size}px`};
padding: 3px;
`;
@@ -920,11 +927,18 @@ export const NiceSelect = styled.div`
background-color: white;
+ border: 2px solid #0003;
border-radius: 0.25rem;
font-size: 1em;
padding: 8px 32px 8px 8px;
/* 0.5em 3em 0.5em 1em; */
cursor: pointer;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: #0000000a;
+ }
}
position: relative;
@@ -1074,7 +1088,9 @@ export const StyledCheckboxLabel = styled.div`
color: #959495;
}
input:focus + div + label {
- box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;
+ box-shadow:
+ 0 0 0 0.05em #fff,
+ 0 0 0.15em 0.1em currentColor;
}
`;
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/index.ts b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
index 6b228188b..d6a14f3dc 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
@@ -14,18 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { AmountJson, AmountString, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
-import { ButtonHandler } from "../../mui/handlers.js";
+import { AmountFieldHandler, ButtonHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
export interface Props {
- talerDepositUri: string | undefined;
- amountStr: AmountString | undefined;
+ account: PaytoUri;
+ scope: ScopeInfo;
cancel: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
@@ -44,7 +44,9 @@ export namespace State {
export interface Ready {
status: "ready";
error: undefined;
+ amount: AmountFieldHandler;
fee: AmountJson;
+ account: PaytoUri;
cost: AmountJson;
effective: AmountJson;
confirm: ButtonHandler;
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
index efcef8c28..f7a52b7dc 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
@@ -14,52 +14,54 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js";
+import { useState } from "preact/hooks";
export function useComponentState({
- talerDepositUri,
- amountStr,
+ account,
+ scope,
cancel,
onSuccess,
}: Props): State {
const api = useBackendContext();
const { pushAlertOnError } = useAlertContext();
- const info = useAsyncAsHook(async () => {
- if (!talerDepositUri) throw Error("ERROR_NO-URI-FOR-DEPOSIT");
- if (!amountStr) throw Error("ERROR_NO-AMOUNT-FOR-DEPOSIT");
- const amount = Amounts.parse(amountStr);
- if (!amount) throw Error("ERROR_INVALID-AMOUNT-FOR-DEPOSIT");
- const deposit = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
- amount: Amounts.stringify(amount),
- depositPaytoUri: talerDepositUri,
+ const [amount, setAmount] = useState(Amounts.zeroOfCurrency(scope.currency))
+ const amountStr = Amounts.stringify(amount);
+
+ const hook = useAsyncAsHook(async () => {
+ const deposit = await api.wallet.call(WalletApiOperation.CheckDeposit, {
+ amount: amountStr,
+ depositPaytoUri: stringifyPaytoUri(account),
});
- return { deposit, uri: talerDepositUri, amount };
- });
- const { i18n } = useTranslationContext();
+ return deposit;
+ }, [amountStr]);
+ // const { i18n } = useTranslationContext();
- if (!info) return { status: "loading", error: undefined };
- if (info.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the status of deposit`,
- info,
- ),
- };
- }
+ // if (!hook) return { status: "loading", error: undefined };
+ // if (hook.hasError) {
+ // return {
+ // status: "error",
+ // error: alertFromError(
+ // i18n,
+ // i18n.str`Could not load the status of deposit`,
+ // hook,
+ // ),
+ // };
+ // }
- const { deposit, uri, amount } = info.response;
+ const debitAmount = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.effectiveDepositAmount);
+ const toBeReceived = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.totalDepositCost);
+ // const { deposit, uri, amount } = hook.response;
async function doDeposit(): Promise<void> {
const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
- amount: Amounts.stringify(amount),
- depositPaytoUri: uri,
+ amount: amountStr,
+ depositPaytoUri: stringifyPaytoUri(account),
});
onSuccess(resp.transactionId);
}
@@ -67,13 +69,19 @@ export function useComponentState({
return {
status: "ready",
error: undefined,
+ account,
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (e) => setAmount(e)),
+ error: Amounts.isZero(amount) ? "Can't be zero" : undefined,
+ },
confirm: {
onClick: pushAlertOnError(doDeposit),
},
- fee: Amounts.sub(deposit.totalDepositCost, deposit.effectiveDepositAmount)
+ fee: Amounts.sub(toBeReceived, debitAmount)
.amount,
- cost: Amounts.parseOrThrow(deposit.totalDepositCost),
- effective: Amounts.parseOrThrow(deposit.effectiveDepositAmount),
+ cost: Amounts.parseOrThrow(toBeReceived),
+ effective: Amounts.parseOrThrow(debitAmount),
cancel,
};
}
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
index cd65ce8e1..aacb7b03d 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import { ReadyView } from "./views.js";
@@ -30,6 +30,10 @@ export default {
export const Ready = tests.createExample(ReadyView, {
status: "ready",
confirm: {},
+ amount: {
+ value: Amounts.parseOrThrow("EUR:1")
+ },
+ account: parsePaytoUri("payto://iban/DE1231231231")!,
cost: Amounts.parseOrThrow("EUR:1.2"),
effective: Amounts.parseOrThrow("EUR:1"),
fee: Amounts.parseOrThrow("EUR:0.2"),
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/test.ts b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
index 100929918..743bd43e5 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountString, Amounts } from "@gnu-taler/taler-util";
+import { AmountString, Amounts, ScopeType, parsePayUri, parsePaytoUri } from "@gnu-taler/taler-util";
import { expect } from "chai";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
@@ -28,63 +28,30 @@ import { Props } from "./index.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
describe("Deposit CTA states", () => {
- it("should tell the user that the URI is missing", async () => {
- const { handler, TestingContext } = createWalletApiMock();
-
- const props: Props = {
- talerDepositUri: undefined,
- amountStr: undefined,
- cancel: async () => {
- null;
- },
- onSuccess: async () => {
- null;
- },
- };
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- useComponentState,
- props,
- [
- ({ status }) => {
- expect(status).equals("loading");
- },
- ({ status, error }) => {
- expect(status).equals("error");
-
- if (!error) expect.fail();
- // if (!error.hasError) expect.fail();
- // if (error.operational) expect.fail();
- expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
- },
- ],
- TestingContext,
- );
-
- expect(hookBehavior).deep.equal({ result: "ok" });
- expect(handler.getCallingQueueState()).eq("empty");
- });
it("should be ready after loading", async () => {
const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(
- WalletApiOperation.PrepareDeposit,
+ WalletApiOperation.CheckDeposit,
undefined,
{
- effectiveDepositAmount: "EUR:1" as AmountString,
- totalDepositCost: "EUR:1.2" as AmountString,
+ effectiveDepositAmount: "EUR:0" as AmountString,
+ totalDepositCost: "EUR:0" as AmountString,
fees: {
coin: "EUR:0" as AmountString,
- refresh: "EUR:0.2" as AmountString,
+ refresh: "EUR:0" as AmountString,
wire: "EUR:0" as AmountString,
},
},
);
const props = {
- talerDepositUri: "payto://refund/asdasdas",
- amountStr: "EUR:1" as AmountString,
+ account: parsePaytoUri("payto://refund/asdasdas")!,
+ scope: {
+ type: ScopeType.Global as const,
+ currency: "EUR",
+ },
cancel: async () => {
null;
},
@@ -97,16 +64,26 @@ describe("Deposit CTA states", () => {
useComponentState,
props,
[
- ({ status }) => {
- expect(status).equals("loading");
+ // ({ status }) => {
+ // expect(status).equals("loading");
+ // },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.confirm.onClick).not.undefined;
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:0"));
},
(state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.confirm.onClick).not.undefined;
- expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
- expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
- expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:0"));
},
],
TestingContext,
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
index c683a755c..3e76b4789 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
@@ -14,12 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { Amount } from "../../components/Amount.js";
-import { Part } from "../../components/Part.js";
+import { AmountField } from "../../components/AmountField.js";
+import { Part, PartPayto } from "../../components/Part.js";
import { Button } from "../../mui/Button.js";
+import { DepositDetails, getAmountWithFee } from "../../wallet/Transaction.js";
import { State } from "./index.js";
/**
@@ -32,29 +33,24 @@ export function ReadyView(state: State.Ready): VNode {
return (
<Fragment>
- <section>
- {Amounts.isNonZero(state.cost) && (
- <Part
- big
- title={i18n.str`Cost`}
- text={<Amount value={state.cost} />}
- kind="negative"
- />
- )}
- {Amounts.isNonZero(state.fee) && (
- <Part
- big
- title={i18n.str`Fee`}
- text={<Amount value={state.fee} />}
- kind="negative"
+ <section style={{ textAlign: "left" }}>
+ <p>
+ <AmountField
+ label={i18n.str`Amount`}
+ handler={state.amount}
+ required
/>
- )}
+ </p>
+ <PartPayto kind="neutral" payto={state.account} />
<Part
- big
- title={i18n.str`To be received`}
- text={<Amount value={state.effective} />}
- kind="positive"
+ title={i18n.str`Details`}
+ text={
+ <DepositDetails
+ amount={getAmountWithFee(state.cost, state.effective, "debit")}
+ />
+ }
/>
+
</section>
<section>
<Button
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
index fd3fb52f8..dbca08b2b 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -14,12 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { AmountJson, AmountString, ScopeInfo } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
-import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { AmountFieldHandler, ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
@@ -27,7 +27,7 @@ import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
export interface Props {
- amount: AmountString;
+ scope: ScopeInfo;
onClose: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
@@ -47,6 +47,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
@@ -58,6 +59,7 @@ export namespace State {
status: "ready";
doSelectExchange: ButtonHandler;
create: ButtonHandler;
+ amount: AmountFieldHandler;
subject: TextFieldHandler;
expiration: TextFieldHandler;
toBeReceived: AmountJson;
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
index daa3ee76d..d2db4f44c 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -15,7 +15,7 @@
*/
/* eslint-disable react-hooks/rules-of-hooks */
-import { Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, AmountString, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { isFuture, parse } from "date-fns";
import { useState } from "preact/hooks";
@@ -28,12 +28,12 @@ import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
export function useComponentState({
- amount: amountStr,
+ scope,
onClose,
onSuccess,
}: Props): RecursiveState<State> {
- const amount = Amounts.parseOrThrow(amountStr);
const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
@@ -49,6 +49,11 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
+ retry: {
+ onClick: pushAlertOnError(async () => {
+ hook.retry();
+ }),
+ },
error: alertFromError(
i18n,
i18n.str`Could not load the list of exchanges`,
@@ -66,12 +71,14 @@ export function useComponentState({
const exchangeList = hook.response.exchanges;
return () => {
+ const [amount, setAmount] = useState<AmountJson>(Amounts.zeroOfCurrency(scope.currency));
const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>();
const { pushAlertOnError } = useAlertContext();
+ const amountStr = Amounts.stringify(amount)
const selectedExchange = useSelectedExchange({
- currency: amount.currency,
+ currency: scope.currency,
defaultExchange: undefined,
list: exchangeList,
});
@@ -91,34 +98,41 @@ export function useComponentState({
},
);
return resp;
- });
-
- if (!hook) {
- return {
- status: "loading",
- error: undefined,
- };
- }
-
- if (hook.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the invoice status`,
- hook,
- ),
- };
- // return {
- // status: "loading-uri",
- // error: hook,
- // };
- }
-
- const { amountEffective, amountRaw } = hook.response;
- const requestAmount = Amounts.parseOrThrow(amountRaw);
- const toBeReceived = Amounts.parseOrThrow(amountEffective);
-
+ },[amountStr]);
+
+ // if (!hook) {
+ // return {
+ // status: "loading",
+ // error: undefined,
+ // };
+ // }
+
+ // if (hook.hasError) {
+ // return {
+ // status: "error",
+ // retry: {
+ // onClick: pushAlertOnError(async () => {
+ // hook.retry();
+ // }),
+ // },
+ // error: alertFromError(
+ // i18n,
+ // i18n.str`Could not load the invoice status`,
+ // hook,
+ // ),
+ // };
+ // // return {
+ // // status: "loading-uri",
+ // // error: hook,
+ // // };
+ // }
+
+ // const { amountEffective, amountRaw } = hook.response;
+ // const requestAmount = Amounts.parseOrThrow(amountRaw);
+ // const toBeReceived = Amounts.parseOrThrow(amountEffective);
+ const requestAmount = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.amountRaw);
+ const toBeReceived = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.amountEffective);
+
let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
let timestampError: string | undefined = undefined;
@@ -163,6 +177,11 @@ export function useComponentState({
return {
status: "ready",
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (e) => setAmount(e)),
+ error: Amounts.isZero(amount) ? "Can't be zero" : undefined,
+ },
subject: {
error:
subject === undefined
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
index 779f130aa..9822f7c91 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -33,6 +33,13 @@ export const Ready = tests.createExample(ReadyView, {
value: 1,
fraction: 0,
},
+ amount: {
+ value: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ }
+ },
expiration: {
value: "2/12/12",
},
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index e2c37fbba..86bac5a16 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -27,10 +27,12 @@ import {
InvoiceCreationDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { AmountField } from "../../components/AmountField.js";
export function ReadyView({
exchangeUrl,
subject,
+ amount,
expiration,
create,
toBeReceived,
@@ -87,6 +89,13 @@ export function ReadyView({
big
/>
<p>
+ <AmountField
+ label={i18n.str`Amount`}
+ handler={amount}
+ required
+ />
+ </p>
+ <p>
<TextField
label="Subject"
variant="filled"
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
index f0cd63fbe..d010ac662 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
@@ -17,14 +17,13 @@
import {
AbsoluteTime,
AmountJson,
- PreparePayResult,
- TalerErrorDetail,
+ PreparePayResult
} from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
@@ -50,6 +49,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index 99de03d2d..deee83751 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -63,6 +63,11 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
+ retry: {
+ onClick: pushAlertOnError(async () => {
+ hook.retry();
+ }),
+ },
error: alertFromError(
i18n,
i18n.str`Could not load the transfer payment status`,
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
index 794d2ad1c..2969efb7f 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
@@ -14,17 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, AmountString, TalerErrorDetail } from "@gnu-taler/taler-util";
+import {
+ AmountJson,
+ AmountString,
+ ScopeInfo,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
-import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { AmountFieldHandler, ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
import { compose, StateViewMap } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
export interface Props {
- amount: AmountString;
+ scope: ScopeInfo;
onClose: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
@@ -39,6 +44,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
@@ -49,6 +55,7 @@ export namespace State {
export interface Ready extends BaseInfo {
status: "ready";
create: ButtonHandler;
+ amount: AmountFieldHandler;
toBeReceived: AmountJson;
debitAmount: AmountJson;
subject: TextFieldHandler;
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
index f092801ed..1a8f318c0 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -15,59 +15,42 @@
*/
import {
- AmountString,
+ AmountJson,
Amounts,
- TalerErrorCode,
- TalerProtocolTimestamp,
+ TalerProtocolTimestamp
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { isFuture, parse } from "date-fns";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { BackgroundError, WxApiType } from "../../wxApi.js";
import { Props, State } from "./index.js";
export function useComponentState({
- amount: amountStr,
+ scope,
onClose,
onSuccess,
}: Props): State {
const api = useBackendContext();
const { pushAlertOnError } = useAlertContext();
- const amount = Amounts.parseOrThrow(amountStr);
+ // const amount = Amounts.parseOrThrow(amountStr);
const { i18n } = useTranslationContext();
+ const [amount, setAmount] = useState<AmountJson>(Amounts.zeroOfCurrency(scope.currency));
const [subject, setSubject] = useState<string | undefined>();
const [timestamp, setTimestamp] = useState<string | undefined>();
+ const amountStr = Amounts.stringify(amount)
const hook = useAsyncAsHook(async () => {
- const resp = await checkPeerPushDebitAndCheckMax(api, amountStr);
- return resp;
- });
-
- if (!hook) {
- return {
- status: "loading",
- error: undefined,
- };
- }
- if (hook.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the max amount to transfer`,
- hook,
- ),
- };
- }
+ return await api.wallet.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: amountStr,
+ });
+ }, [amountStr]);
- const { amountEffective, amountRaw } = hook.response;
- const debitAmount = Amounts.parseOrThrow(amountEffective);
- const toBeReceived = Amounts.parseOrThrow(amountRaw);
+ const debitAmount = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.amountEffective);
+ const toBeReceived = !hook || hook.hasError ? Amounts.zeroOfCurrency(scope.currency) : Amounts.parseOrThrow(hook.response.amountRaw);
let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
let timestampError: string | undefined = undefined;
@@ -107,13 +90,18 @@ export function useComponentState({
}
const unableToCreate =
- !subject || Amounts.isZero(amount) || !purse_expiration;
+ !subject || Amounts.isZero(amount) || Amounts.isZero(debitAmount) || !purse_expiration;
return {
status: "ready",
cancel: {
onClick: pushAlertOnError(onClose),
},
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (e) => setAmount(e)),
+ error: Amounts.isZero(amount) ? "Can't be zero" : undefined,
+ },
subject: {
error:
subject === undefined
@@ -140,46 +128,3 @@ export function useComponentState({
};
}
-async function checkPeerPushDebitAndCheckMax(
- api: WxApiType,
- amountState: AmountString,
-) {
- // FIXME : https://bugs.gnunet.org/view.php?id=7872
- try {
- return await api.wallet.call(WalletApiOperation.CheckPeerPushDebit, {
- amount: amountState,
- });
- } catch (e) {
- if (!(e instanceof BackgroundError)) {
- throw e;
- }
- if (
- !e.hasErrorCode(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- )
- ) {
- throw e;
- }
- const material = Amounts.parseOrThrow(
- e.errorDetail.insufficientBalanceDetails.balanceMaterial,
- );
- const amount = Amounts.parseOrThrow(amountState);
- const gap = Amounts.sub(
- amount,
- Amounts.parseOrThrow(
- e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount,
- ),
- ).amount;
- const newAmount = Amounts.sub(material, gap).amount;
- if (Amounts.cmp(newAmount, amount) === 0) {
- //insufficient balance and the exception didn't give
- //a good response that allow us to try again
- throw e;
- }
- if (Amounts.cmp(newAmount, amount) === 1) {
- //how can this happen?
- throw e;
- }
- return checkPeerPushDebitAndCheckMax(api, Amounts.stringify(newAmount));
- }
-}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
index 8e9fbbe63..9ba806ba4 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
@@ -33,6 +33,13 @@ export const Ready = tests.createExample(ReadyView, {
value: 1,
fraction: 0,
},
+ amount: {
+ value: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ }
+ },
expiration: {
value: "20/1/2022",
},
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
index bc855f33d..269f39fbe 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
@@ -25,9 +25,11 @@ import {
TransferCreationDetails,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { AmountField } from "../../components/AmountField.js";
export function ReadyView({
subject,
+ amount,
expiration,
toBeReceived,
debitAmount,
@@ -61,6 +63,13 @@ export function ReadyView({
<Fragment>
<section style={{ textAlign: "left" }}>
<p>
+ <AmountField
+ label={i18n.str`Amount`}
+ handler={amount}
+ required
+ />
+ </p>
+ <p>
<TextField
label="Subject"
variant="filled"
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
index 4e1301d6a..a7bb0b67a 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
@@ -14,16 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- AbsoluteTime,
- AmountJson,
- TalerErrorDetail,
-} from "@gnu-taler/taler-util";
+import { AbsoluteTime, AmountJson } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { ButtonHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
@@ -43,6 +39,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
index 67f6d9113..28d8c9e70 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
@@ -49,6 +49,11 @@ export function useComponentState({
if (hook.hasError) {
return {
status: "error",
+ retry: {
+ onClick: pushAlertOnError(async () => {
+ hook.retry();
+ }),
+ },
error: alertFromError(
i18n,
i18n.str`Could not load the invoice payment status`,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index af1ef213b..91bde9369 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -16,9 +16,9 @@
import {
AmountJson,
- AmountString,
CurrencySpecification,
- ExchangeListItem
+ ExchangeListItem,
+ ScopeInfo
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
@@ -37,7 +37,7 @@ import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { ErrorAlert } from "../../context/alert.js";
import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
-import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js";
+import { FinalStateOperation, SuccessView } from "./views.js";
export interface PropsFromURI {
talerWithdrawUri: string | undefined;
@@ -47,10 +47,10 @@ export interface PropsFromURI {
export interface PropsFromParams {
talerExchangeWithdrawUri: string | undefined;
+ scope: ScopeInfo;
amount: string | undefined;
cancel: () => Promise<void>;
onSuccess: (txid: string) => Promise<void>;
- onAmountChanged: (amount: AmountString) => Promise<void>;
}
export type State =
@@ -58,7 +58,7 @@ export type State =
| State.LoadingUriError
| SelectExchangeState.NoExchangeFound
| SelectExchangeState.Selecting
- | State.SelectAmount
+ // | State.SelectAmount
| State.AlreadyCompleted
| State.Success;
@@ -72,20 +72,20 @@ export namespace State {
error: ErrorAlert;
}
- export interface SelectAmount {
- status: "select-amount";
- error: undefined;
- exchangeBaseUrl: string;
- confirm: ButtonHandler;
- amount: AmountFieldHandler;
- currency: string;
- }
+ // export interface SelectAmount {
+ // status: "select-amount";
+ // error: undefined;
+ // exchangeBaseUrl: string;
+ // confirm: ButtonHandler;
+ // amount: AmountFieldHandler;
+ // currency: string;
+ // }
export interface AlreadyCompleted {
status: "already-completed";
operationState: "confirmed" | "aborted" | "selected";
thisWallet: boolean;
redirectToTx: () => void;
- confirmTransferUrl?: string,
+ confirmTransferUrl?: string;
error: undefined;
}
@@ -99,8 +99,8 @@ export namespace State {
editableAmount: boolean;
bankFee: AmountJson;
- withdrawalFee: AmountJson;
toBeReceived: AmountJson;
+ toBeSent: AmountJson;
doWithdrawal: ButtonHandler;
doSelectExchange: ButtonHandler;
@@ -109,10 +109,12 @@ export namespace State {
chooseCurrencies: string[];
selectedCurrency: string;
changeCurrency: (s: string) => void;
- conversionInfo: {
- spec: CurrencySpecification,
- amount: AmountJson,
- } | undefined;
+ conversionInfo:
+ | {
+ spec: CurrencySpecification;
+ amount: AmountJson;
+ }
+ | undefined;
ageRestriction?: SelectFieldHandler;
@@ -124,7 +126,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
- "select-amount": SelectAmountView,
+ // "select-amount": SelectAmountView,
"no-exchange-found": NoExchangesView,
"selecting-exchange": ExchangeSelectionPage,
success: SuccessView,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index f8e27e688..8a862d200 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -22,6 +22,7 @@ import {
ExchangeListItem,
NotificationType,
TransactionMajorState,
+ TransactionMinorState,
parseWithdrawExchangeUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -37,8 +38,8 @@ import { PropsFromParams, PropsFromURI, State } from "./index.js";
export function useComponentStateFromParams({
talerExchangeWithdrawUri: maybeTalerUri,
amount,
+ scope,
cancel,
- onAmountChanged,
onSuccess,
}: PropsFromParams): RecursiveState<State> {
const api = useBackendContext();
@@ -96,66 +97,66 @@ export function useComponentStateFromParams({
const maybeAmount = uriInfoHook.response.amount ?? paramsAmount;
- if (!maybeAmount) {
- const exchangeBaseUrl =
- uriInfoHook.response.exchange?.exchangeBaseUrl ??
- (exchangeList.length > 0 ? exchangeList[0].exchangeBaseUrl : undefined);
- const currency =
- uriInfoHook.response.exchange?.currency ??
- (exchangeList.length > 0 ? exchangeList[0].currency : undefined);
-
- if (!exchangeBaseUrl) {
- return {
- status: "error",
- error: {
- message: i18n.str`Can't withdraw from exchange`,
- description: i18n.str`Missing base URL`,
- cause: undefined,
- context: {},
- type: "error",
- },
- };
- }
- if (!currency) {
- return {
- status: "error",
- error: {
- message: i18n.str`Can't withdraw from exchange`,
- description: i18n.str`Missing unknown currency`,
- cause: undefined,
- context: {},
- type: "error",
- },
- };
- }
- return () => {
- const { pushAlertOnError } = useAlertContext();
- const [amount, setAmount] = useState<AmountJson>(
- Amounts.zeroOfCurrency(currency),
- );
- const isValid = Amounts.isNonZero(amount);
- return {
- status: "select-amount",
- currency,
- exchangeBaseUrl,
- error: undefined,
- confirm: {
- onClick: isValid
- ? pushAlertOnError(async () => {
- onAmountChanged(Amounts.stringify(amount));
- })
- : undefined,
- },
- amount: {
- value: amount,
- onInput: pushAlertOnError(async (e) => {
- setAmount(e);
- }),
- },
- };
- };
- }
- const chosenAmount = maybeAmount;
+ // if (!maybeAmount) {
+ // const exchangeBaseUrl =
+ // uriInfoHook.response.exchange?.exchangeBaseUrl ??
+ // (exchangeList.length > 0 ? exchangeList[0].exchangeBaseUrl : undefined);
+ // const currency =
+ // uriInfoHook.response.exchange?.currency ??
+ // (exchangeList.length > 0 ? exchangeList[0].currency : undefined);
+
+ // if (!exchangeBaseUrl) {
+ // return {
+ // status: "error",
+ // error: {
+ // message: i18n.str`Can't withdraw from exchange`,
+ // description: i18n.str`Missing base URL`,
+ // cause: undefined,
+ // context: {},
+ // type: "error",
+ // },
+ // };
+ // }
+ // if (!currency) {
+ // return {
+ // status: "error",
+ // error: {
+ // message: i18n.str`Can't withdraw from exchange`,
+ // description: i18n.str`Missing unknown currency`,
+ // cause: undefined,
+ // context: {},
+ // type: "error",
+ // },
+ // };
+ // }
+ // return () => {
+ // const { pushAlertOnError } = useAlertContext();
+ // const [amount, setAmount] = useState<AmountJson>(
+ // Amounts.zeroOfCurrency(currency),
+ // );
+ // const isValid = Amounts.isNonZero(amount);
+ // return {
+ // status: "select-amount",
+ // currency,
+ // exchangeBaseUrl,
+ // error: undefined,
+ // confirm: {
+ // onClick: isValid
+ // ? pushAlertOnError(async () => {
+ // onAmountChanged(Amounts.stringify(amount));
+ // })
+ // : undefined,
+ // },
+ // amount: {
+ // value: amount,
+ // onInput: pushAlertOnError(async (e) => {
+ // setAmount(e);
+ // }),
+ // },
+ // };
+ // };
+ // }
+ const chosenAmount = maybeAmount ?? Amounts.zeroOfCurrency(scope.currency);
async function doManualWithdraw(
exchange: string,
@@ -207,6 +208,7 @@ export function useComponentStateFromURI({
const api = useBackendContext();
const { i18n } = useTranslationContext();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
/**
* Ask the wallet about the withdraw URI
@@ -315,11 +317,14 @@ export function useComponentStateFromURI({
uriInfoHook.response.status !== "pending"
) {
const info = uriInfoHook.response.txInfo;
+ const anotherWallet =
+ info.txState.major === TransactionMajorState.Aborted &&
+ info.txState.minor === TransactionMinorState.CompletedByOtherWallet;
return {
status: "already-completed",
operationState: uriInfoHook.response.status,
confirmTransferUrl: bwi.confirmTransferUrl,
- thisWallet: info.txState.major === TransactionMajorState.Pending,
+ thisWallet: !anotherWallet,
redirectToTx: () => onSuccess(info.transactionId),
error: undefined,
};
@@ -391,7 +396,6 @@ function exchangeSelectionState(
const safeAmount = wInfo.amount
? wInfo.amount
: Amounts.zeroOfCurrency(wInfo.currency);
- const [choosenAmount, setChoosenAmount] = useState(safeAmount);
if (selectedExchange.status !== "ready") {
return selectedExchange;
@@ -403,8 +407,9 @@ function exchangeSelectionState(
| State.Loading => {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
+
+ const [choosenAmount, setChoosenAmount] = useState(safeAmount);
const [ageRestricted, setAgeRestricted] = useState(0);
- const currentExchange = selectedExchange.selected;
const [selectedCurrency, setSelectedCurrency] = useState<string>(
wInfo.currency,
@@ -417,7 +422,7 @@ function exchangeSelectionState(
const info = await api.wallet.call(
WalletApiOperation.GetWithdrawalDetailsForAmount,
{
- exchangeBaseUrl: currentExchange.exchangeBaseUrl,
+ exchangeBaseUrl: selectedExchange.selected.exchangeBaseUrl,
amount: Amounts.stringify(choosenAmount),
restrictAge: ageRestricted,
},
@@ -430,13 +435,33 @@ function exchangeSelectionState(
return {
amount: withdrawAmount,
+ currentExchange: selectedExchange.selected,
ageRestrictionOptions: info.ageRestrictionOptions,
accounts: info.withdrawalAccountsList,
};
- }, []);
+ }, [choosenAmount, selectedExchange.selected, ageRestricted]);
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+ if (!amountHook) {
+ return { status: "loading", error: undefined };
+ }
+ if (amountHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the withdrawal details`,
+ amountHook,
+ ),
+ };
+ }
+ if (!amountHook.response) {
+ return { status: "loading", error: undefined };
+ }
+
+ const currentExchange = amountHook.response.currentExchange;
+
async function doWithdrawAndCheckError(): Promise<void> {
try {
setDoingWithdraw(true);
@@ -458,30 +483,10 @@ function exchangeSelectionState(
setDoingWithdraw(false);
}
- if (!amountHook) {
- return { status: "loading", error: undefined };
- }
- if (amountHook.hasError) {
- return {
- status: "error",
- error: alertFromError(
- i18n,
- i18n.str`Could not load the withdrawal details`,
- amountHook,
- ),
- };
- }
- if (!amountHook.response) {
- return { status: "loading", error: undefined };
- }
-
- const withdrawalFee = Amounts.sub(
- amountHook.response.amount.raw,
- amountHook.response.amount.effective,
- ).amount;
+ const toBeSent = amountHook.response.amount.raw;
const toBeReceived = amountHook.response.amount.effective;
- const bankFee = wInfo.amount;
+ const bankFee = wInfo.bankFee;
const ageRestrictionOptions =
amountHook.response.ageRestrictionOptions?.reduce(
@@ -508,10 +513,7 @@ function exchangeSelectionState(
const altCurrencies = amountHook.response.accounts
.filter((a) => !!a.currencySpecification)
.map((a) => a.currencySpecification!.name);
- const chooseCurrencies =
- altCurrencies.length === 0
- ? []
- : [toBeReceived.currency, ...altCurrencies];
+ const chooseCurrencies = altCurrencies.length <= 1 ? [] : altCurrencies;
const convAccount = amountHook.response.accounts.find((c) => {
return (
@@ -528,8 +530,11 @@ function exchangeSelectionState(
const amountError = Amounts.isZero(choosenAmount)
? i18n.str`should be greater than zero`
- : Amounts.cmp(choosenAmount, wInfo.maxAmount) === -1
- ? i18n.str`choose a lower value`
+ : Amounts.isNonZero(wInfo.maxAmount) &&
+ Amounts.cmp(choosenAmount, wInfo.maxAmount) === 1
+ ? i18n.str`can't be greater than ${Amounts.stringifyValue(
+ wInfo.maxAmount,
+ )}`
: undefined;
return {
@@ -544,6 +549,7 @@ function exchangeSelectionState(
editableExchange: wInfo.editableExchange,
currentExchange,
toBeReceived,
+ toBeSent,
chooseCurrencies,
bankFee,
selectedCurrency,
@@ -551,7 +557,6 @@ function exchangeSelectionState(
setSelectedCurrency(s);
},
conversionInfo,
- withdrawalFee,
amount: {
value: choosenAmount,
onInput: wInfo.editableAmount
@@ -565,11 +570,11 @@ function exchangeSelectionState(
ageRestriction,
doWithdrawal: {
onClick:
- doingWithdraw && !amountError
+ doingWithdraw || amountError
? undefined
: pushAlertOnError(doWithdrawAndCheckError),
},
cancel,
};
- }, []);
+ }, [selectedExchange.selected]);
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index 1bfafb231..d9b7c380e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -48,14 +48,20 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 10000000,
- }
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -72,20 +78,19 @@ export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
export const AlreadyAborted = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "aborted"
+ operationState: "aborted",
});
export const AlreadySelected = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "selected"
+ operationState: "selected",
});
export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
error: undefined,
status: "already-completed",
- operationState: "confirmed"
+ operationState: "confirmed",
});
-
export const WithSomeFee = tests.createExample(SuccessView, {
error: undefined,
status: "success",
@@ -94,14 +99,20 @@ export const WithSomeFee = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 10000000,
- }
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 10000000,
value: 1,
@@ -123,14 +134,20 @@ export const WithoutFee = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 0,
- }
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -152,14 +169,20 @@ export const EditExchangeUntouched = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 10000000,
- }
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -181,14 +204,20 @@ export const EditExchangeModified = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 10000000,
- }
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -211,15 +240,21 @@ export const WithAgeRestriction = tests.createExample(SuccessView, {
currency: "USD",
value: 2,
fraction: 10000000,
- }
+ },
},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+
doSelectExchange: {},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.demo.taler.net",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "USD",
fraction: 0,
value: 0,
@@ -240,8 +275,14 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
currency: "NETZBON",
value: 2,
fraction: 10000000,
- }
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "NETZBON",
doWithdrawal: { onClick: nullFunction },
@@ -249,7 +290,7 @@ export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -270,27 +311,33 @@ export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
currency: "NETZBON",
value: 2,
fraction: 10000000,
- }
+ },
+ },
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
},
+
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 1,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
@@ -311,27 +358,32 @@ export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, {
currency: "NETZBON",
value: 2,
fraction: 10000000,
- }
+ },
},
chooseCurrencies: ["NETZBON", "EUR"],
selectedCurrency: "EUR",
- changeCurrency: () => { },
+ changeCurrency: () => {},
+ bankFee: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
conversionInfo: {
spec: {
- name: "EUR"
+ name: "EUR",
} as CurrencySpecification,
amount: {
currency: "EUR",
fraction: 10000000,
value: 2,
- }
+ },
},
doWithdrawal: { onClick: nullFunction },
currentExchange: {
exchangeBaseUrl: "https://exchange.netzbon.ch",
tos: {},
} as Partial<ExchangeListItem> as any,
- withdrawalFee: {
+ toBeSent: {
currency: "NETZBON",
fraction: 10000000,
value: 1,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index bce5f71e3..5a75cb4be 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -123,7 +123,7 @@ describe("Withdraw CTA states", () => {
editableExchange: false,
maxAmount: "ARS:1",
wireFee: "ARS:0",
- },
+ },
},
);
@@ -208,7 +208,7 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
@@ -302,7 +302,7 @@ describe("Withdraw CTA states", () => {
if (state.status !== "success") return;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
- expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
+ expect(state.toBeSent).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.amount.value).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined;
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 86d7248a4..3283f998f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -42,6 +42,7 @@ import { State } from "./index.js";
export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
const { i18n } = useTranslationContext();
+
// document.location.href = res.confirmTransferUrl
if (state.thisWallet) {
switch (state.operationState) {
@@ -143,8 +144,6 @@ export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
export function SuccessView(state: State.Success): VNode {
const { i18n } = useTranslationContext();
- // const currentTosVersionIsAccepted =
- // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
return (
<Fragment>
<section style={{ textAlign: "left" }}>
@@ -212,9 +211,10 @@ export function SuccessView(state: State.Success): VNode {
conversion={state.conversionInfo?.amount}
amount={getAmountWithFee(
state.toBeReceived,
- state.amount.value,
+ state.toBeSent,
"credit",
)}
+ bankFee={state.bankFee}
/>
}
/>
@@ -232,7 +232,6 @@ export function SuccessView(state: State.Success): VNode {
</section>
<section>
- {/* <div> */}
<TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
<Button
variant="contained"
@@ -245,20 +244,6 @@ export function SuccessView(state: State.Success): VNode {
</i18n.Translate>
</Button>
</TermsOfService>
- {/* </div>
- <div style={{ marginTop: 20 }}>
- <Button
- variant="text"
- color="success"
-
- disabled={!state.doAbort.onClick}
- onClick={state.doAbort.onClick}
- >
- <i18n.Translate>
- Cancel
- </i18n.Translate>
- </Button>
- </div> */}
</section>
{state.talerWithdrawUri ? (
<WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
@@ -295,44 +280,3 @@ function WithdrawWithMobile({
);
}
-export function SelectAmountView({
- amount,
- exchangeBaseUrl,
- confirm,
-}: State.SelectAmount): VNode {
- const { i18n } = useTranslationContext();
- return (
- <Fragment>
- <section style={{ textAlign: "left" }}>
- <Part
- title={
- <div
- style={{
- display: "flex",
- alignItems: "center",
- }}
- >
- <i18n.Translate>Exchange</i18n.Translate>
- </div>
- }
- text={<ExchangeDetails exchange={exchangeBaseUrl} />}
- kind="neutral"
- big
- />
- <Grid container columns={2} justifyContent="space-between">
- <AmountField label={i18n.str`Amount`} required handler={amount} />
- </Grid>
- </section>
- <section>
- <Button
- variant="contained"
- color="info"
- disabled={!confirm.onClick}
- onClick={confirm.onClick}
- >
- <i18n.Translate>See details</i18n.Translate>
- </Button>
- </section>
- </Fragment>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/cta/termsExample.ts b/packages/taler-wallet-webextension/src/cta/termsExample.ts
index ba0bee89e..0e5ff6d4b 100644
--- a/packages/taler-wallet-webextension/src/cta/termsExample.ts
+++ b/packages/taler-wallet-webextension/src/cta/termsExample.ts
@@ -21,17 +21,18 @@
*/
export const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
-<head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title>Terms Of Service &#8212; Taler Terms of Service</title>
-</head><body>
- <div>
- Terms of service
- </div>
- <div>
- A complete separated html with it's own design
- </div>
-</body>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Terms Of Service &#8212; Taler Terms of Service</title>
+ </head>
+ <body>
+ <div>
+ Terms of service
+ </div>
+ <div>
+ A complete separated html with it's own design
+ </div>
+ </body>
</html>
`;
export const termsPlain = `
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index bc66f2136..73b619c8e 100644
--- a/packages/taler-wallet-webextension/src/i18n/de.po
+++ b/packages/taler-wallet-webextension/src/i18n/de.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-05-07 14:32+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2024-09-26 05:33+0000\n"
+"Last-Translator: LukBru <zur@posteo.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/de/>\n"
"Language: de\n"
@@ -26,12 +26,12 @@ msgstr ""
"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.4.3\n"
+"X-Generator: Weblate 5.5.5\n"
#: src/NavigationBar.tsx:139
#, c-format
msgid "Balance"
-msgstr "Guthaben"
+msgstr "Saldo"
#: src/NavigationBar.tsx:142
#, c-format
@@ -61,7 +61,7 @@ msgstr "%1$s"
#: src/components/PendingTransactions.tsx:74
#, c-format
msgid "PENDING OPERATIONS"
-msgstr ""
+msgstr "AUSSTEHENDE VORGÄNGE"
#: src/components/Loading.tsx:36
#, c-format
@@ -81,7 +81,7 @@ msgstr ""
#: src/wallet/BackupPage.tsx:205
#, c-format
msgid "Add provider"
-msgstr ""
+msgstr "Anbieter hinzufügen"
#: src/wallet/BackupPage.tsx:219
#, c-format
@@ -91,22 +91,22 @@ msgstr ""
#: src/wallet/BackupPage.tsx:221
#, c-format
msgid "Sync now"
-msgstr ""
+msgstr "Jetzt synchronisieren"
#: src/wallet/BackupPage.tsx:264
#, c-format
msgid "Last synced"
-msgstr ""
+msgstr "Zuletzt synchronisiert"
#: src/wallet/BackupPage.tsx:269
#, c-format
msgid "Not synced"
-msgstr ""
+msgstr "Nicht synchronisiert"
#: src/wallet/BackupPage.tsx:289
#, c-format
msgid "Expires in"
-msgstr ""
+msgstr "Läuft ab in"
#: src/wallet/ProviderDetailPage.tsx:60
#, c-format
@@ -121,32 +121,32 @@ msgstr ""
#: src/wallet/ProviderDetailPage.tsx:115
#, c-format
msgid "See providers"
-msgstr ""
+msgstr "Zahlungsdienste ansehen"
#: src/wallet/ProviderDetailPage.tsx:143
#, c-format
msgid "Last backup"
-msgstr ""
+msgstr "Letztes Backup"
#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
msgid "Back up"
-msgstr ""
+msgstr "Backup ausführen"
#: src/wallet/ProviderDetailPage.tsx:154
#, c-format
msgid "Provider fee"
-msgstr ""
+msgstr "Anbietergebühr"
#: src/wallet/ProviderDetailPage.tsx:157
#, c-format
msgid "per year"
-msgstr ""
+msgstr "pro Jahr"
#: src/wallet/ProviderDetailPage.tsx:163
#, c-format
msgid "Extend"
-msgstr ""
+msgstr "Erweitern"
#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
@@ -160,17 +160,17 @@ msgstr ""
#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
msgid "old"
-msgstr ""
+msgstr "alt"
#: src/wallet/ProviderDetailPage.tsx:183
#, c-format
msgid "new"
-msgstr ""
+msgstr "neu"
#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
msgid "fee"
-msgstr ""
+msgstr "Gebühr"
#: src/wallet/ProviderDetailPage.tsx:198
#, c-format
@@ -180,12 +180,12 @@ msgstr ""
#: src/wallet/ProviderDetailPage.tsx:215
#, c-format
msgid "Remove provider"
-msgstr ""
+msgstr "Anbieter entfernen"
#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
msgid "This provider has reported an error"
-msgstr ""
+msgstr "Dieser Anbieter hat einen Fehler gemeldet"
#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
@@ -215,7 +215,7 @@ msgstr ""
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr "Zurück"
+msgstr "Abbrechen"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -532,7 +532,7 @@ msgstr ""
#: src/components/BankDetailsByPaytoType.tsx:148
#, c-format
msgid "Subject"
-msgstr "Verwendungszweck"
+msgstr "Buchungsvermerk"
#: src/components/BankDetailsByPaytoType.tsx:154
#, c-format
@@ -589,7 +589,7 @@ msgstr "Bestätigen"
#: src/wallet/Transaction.tsx:267
#, c-format
msgid "Withdrawal"
-msgstr "Abheben"
+msgstr "Abhebung"
#: src/wallet/Transaction.tsx:286
#, c-format
@@ -1025,7 +1025,8 @@ msgstr "Ich akzeptiere die Allgemeinen Geschäftsbedingungen (AGB)"
#: src/components/TermsOfService/views.tsx:107
#, c-format
msgid "Exchange doesn&apos;t have terms of service"
-msgstr "Dieser Exchange hat keine Allgemeine Geschäftsbedingungen (AGB)"
+msgstr ""
+"Dieser Zahlungsdienstleister hat keine Allgemeinen Geschäftsbedingungen (AGB)"
#: src/components/TermsOfService/views.tsx:135
#, c-format
@@ -1365,7 +1366,7 @@ msgstr "Allgemeine Geschäftsbedingungen (AGB) ansehen"
#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
msgid "Exchange URL"
-msgstr ""
+msgstr "URL des Exchange"
#: src/wallet/ExchangeAddConfirm.tsx:70
#, c-format
@@ -1415,7 +1416,7 @@ msgstr ""
#: src/wallet/ExchangeSetUrl.tsx:206
#, c-format
msgid "Next"
-msgstr ""
+msgstr "Nächste"
#: src/components/TransactionItem.tsx:201
#, c-format
@@ -1460,7 +1461,7 @@ msgstr ""
#: src/wallet/ProviderAddPage.tsx:158
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Name"
#: src/wallet/ProviderAddPage.tsx:212
#, c-format
@@ -1735,8 +1736,8 @@ msgid ""
"Please check in your %1$s settings that you have IndexedDB enabled (check "
"the preference name %2$s)."
msgstr ""
-"Bitte prüfen Sie ihre %1$s Einstellungen, für die Sie IndexedDB verwenden ("
-"preference name %2$s prüfen)."
+"Bitte prüfen Sie in Ihren %1$s-Einstellungen, dass Sie IndexedDB aktiviert "
+"haben, und überprüfen Sie %2$s."
#: src/components/Diagnostics.tsx:70
#, c-format
@@ -1978,7 +1979,7 @@ msgstr ""
#: src/components/EditableText.tsx:45
#, c-format
msgid "Edit"
-msgstr ""
+msgstr "Bearbeiten"
#: src/wallet/ManualWithdrawPage.tsx:102
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/es.po b/packages/taler-wallet-webextension/src/i18n/es.po
index ea1fa9803..eb43e2949 100644
--- a/packages/taler-wallet-webextension/src/i18n/es.po
+++ b/packages/taler-wallet-webextension/src/i18n/es.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-03-07 07:03+0000\n"
-"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
+"PO-Revision-Date: 2024-07-03 18:32+0000\n"
+"Last-Translator: Luis Avalos <avalos.diaz.0577@gmail.com>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/es/>\n"
"Language: es\n"
@@ -26,7 +26,7 @@ msgstr ""
"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"
+"X-Generator: Weblate 5.5.5\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -1386,7 +1386,7 @@ msgstr "Revisar los términos de servicio"
#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
msgid "Exchange URL"
-msgstr "URL del Exchange"
+msgstr "URL del proveedor"
#: src/wallet/ExchangeAddConfirm.tsx:70
#, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/tr.po b/packages/taler-wallet-webextension/src/i18n/tr.po
index 5848b9f3a..3af81d0bc 100644
--- a/packages/taler-wallet-webextension/src/i18n/tr.po
+++ b/packages/taler-wallet-webextension/src/i18n/tr.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-03-08 01:14+0000\n"
-"Last-Translator: Alp <berna.alp@digitalekho.com>\n"
+"PO-Revision-Date: 2024-09-14 05:26+0000\n"
+"Last-Translator: Muha Aliss <muhaaliss@tuta.io>\n"
"Language-Team: Turkish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/tr/>\n"
"Language: tr\n"
@@ -26,7 +26,7 @@ msgstr ""
"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"
+"X-Generator: Weblate 5.5.5\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -41,7 +41,7 @@ msgstr "Yedekle"
#: src/NavigationBar.tsx:147
#, c-format
msgid "QR Reader and Taler URI"
-msgstr ""
+msgstr "QR Okuyucu ve Taler URI'si"
#: src/NavigationBar.tsx:154
#, c-format
@@ -54,14 +54,14 @@ msgid "Dev"
msgstr "Gelişim"
#: src/mui/Typography.tsx:122
-#, fuzzy, c-format
+#, c-format
msgid "%1$s"
msgstr "%1$s"
#: src/components/PendingTransactions.tsx:74
#, c-format
msgid "PENDING OPERATIONS"
-msgstr ""
+msgstr "BEKLEYEN İŞLEMLER"
#: src/components/Loading.tsx:36
#, c-format
@@ -111,42 +111,42 @@ msgstr "İçinde sona eriyor"
#: src/wallet/ProviderDetailPage.tsx:60
#, c-format
msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
-msgstr ""
+msgstr "&quot;%1$s&quot; için sağlayıcı ayrıntısı yüklenirken bir hata oluştu"
#: src/wallet/ProviderDetailPage.tsx:108
#, c-format
msgid "There is not known provider with url &quot;%1$s&quot;."
-msgstr ""
+msgstr "&quot;%1$s&quot; URL'sine sahip bilinen bir sağlayıcı yok."
#: src/wallet/ProviderDetailPage.tsx:115
-#, fuzzy, c-format
+#, c-format
msgid "See providers"
-msgstr "Sağlayıcı ekle"
+msgstr "Sağlayıcıları gör"
#: src/wallet/ProviderDetailPage.tsx:143
#, c-format
msgid "Last backup"
-msgstr ""
+msgstr "Son yedek"
#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
msgid "Back up"
-msgstr ""
+msgstr "Yedekle"
#: src/wallet/ProviderDetailPage.tsx:154
#, c-format
msgid "Provider fee"
-msgstr ""
+msgstr "Sağlayıcı ücreti"
#: src/wallet/ProviderDetailPage.tsx:157
#, c-format
msgid "per year"
-msgstr ""
+msgstr "yıllık"
#: src/wallet/ProviderDetailPage.tsx:163
#, c-format
msgid "Extend"
-msgstr ""
+msgstr "Uzat"
#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
@@ -154,46 +154,48 @@ msgid ""
"terms has changed, extending the service will imply accepting the new terms "
"of service"
msgstr ""
+"şartlar değişti; hizmetin uzatılması yeni hizmet şartlarının kabul edilmesi "
+"anlamına gelecektir"
#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
msgid "old"
-msgstr ""
+msgstr "eski"
#: src/wallet/ProviderDetailPage.tsx:183
#, c-format
msgid "new"
-msgstr ""
+msgstr "yeni"
#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
msgid "fee"
-msgstr ""
+msgstr "ücret"
#: src/wallet/ProviderDetailPage.tsx:198
#, c-format
msgid "storage"
-msgstr ""
+msgstr "depolama"
#: src/wallet/ProviderDetailPage.tsx:215
#, c-format
msgid "Remove provider"
-msgstr ""
+msgstr "Sağlayıcıyı kaldır"
#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
msgid "This provider has reported an error"
-msgstr ""
+msgstr "Sağlayıcı bir hata bildirdi"
#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
msgid "There is conflict with another backup from %1$s"
-msgstr ""
+msgstr "%1$s başka bir yedekle çakışıyor"
#: src/wallet/ProviderDetailPage.tsx:253
#, c-format
msgid "Backup is not readable"
-msgstr ""
+msgstr "Yedek okunamıyor"
#: src/wallet/ProviderDetailPage.tsx:261
#, c-format
@@ -308,47 +310,47 @@ msgstr "Reddet"
#: src/popup/Application.tsx:177
#, c-format
msgid "this popup is being closed and you are being redirected to %1$s"
-msgstr ""
+msgstr "bu açılır pencere kapatılıyor ve %1$s adresine yönlendiriliyorsunuz"
#: src/components/ShowFullContractTermPopup.tsx:158
-#, fuzzy, c-format
+#, c-format
msgid "Could not load purchase proposal details"
-msgstr "Yedekleme sağlayıcıları yüklenemedi"
+msgstr "Satın alma teklifi ayrıntıları yüklenemedi"
#: src/components/ShowFullContractTermPopup.tsx:183
-#, fuzzy, c-format
+#, c-format
msgid "Order Id"
-msgstr "Sipariş reddedildi"
+msgstr "Sipariş kimliği"
#: src/components/ShowFullContractTermPopup.tsx:189
#, c-format
msgid "Summary"
-msgstr ""
+msgstr "Özet"
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Miktar"
#: src/components/ShowFullContractTermPopup.tsx:203
#, c-format
msgid "Merchant name"
-msgstr ""
+msgstr "Satıcı adı"
#: src/components/ShowFullContractTermPopup.tsx:209
-#, c-format
+#, c-format, fuzzy
msgid "Merchant jurisdiction"
-msgstr ""
+msgstr "Satıcının yetki alanı"
#: src/components/ShowFullContractTermPopup.tsx:215
#, c-format
msgid "Merchant address"
-msgstr ""
+msgstr "Satıcı adresi"
#: src/components/ShowFullContractTermPopup.tsx:221
#, c-format
msgid "Merchant logo"
-msgstr ""
+msgstr "Satıcı logosu"
#: src/components/ShowFullContractTermPopup.tsx:234
#, c-format
@@ -363,112 +365,112 @@ msgstr "Satıcı e-postası"
#: src/components/ShowFullContractTermPopup.tsx:246
#, c-format
msgid "Merchant public key"
-msgstr ""
+msgstr "Satıcı genel anahtarı"
#: src/components/ShowFullContractTermPopup.tsx:256
#, c-format
msgid "Delivery date"
-msgstr ""
+msgstr "Teslim tarihi"
#: src/components/ShowFullContractTermPopup.tsx:271
-#, fuzzy, c-format
+#, c-format
msgid "Delivery location"
-msgstr "Taler Eylemi"
+msgstr "Teslimat yeri"
#: src/components/ShowFullContractTermPopup.tsx:277
#, c-format
msgid "Products"
-msgstr ""
+msgstr "Ürünler"
#: src/components/ShowFullContractTermPopup.tsx:289
#, c-format
msgid "Created at"
-msgstr ""
+msgstr "Oluşturulma"
#: src/components/ShowFullContractTermPopup.tsx:304
#, c-format
msgid "Refund deadline"
-msgstr ""
+msgstr "Geri ödeme son tarihi"
#: src/components/ShowFullContractTermPopup.tsx:319
-#, fuzzy, c-format
+#, c-format
msgid "Auto refund"
-msgstr "Ödeme iadesi"
+msgstr "Otomatik geri ödeme"
#: src/components/ShowFullContractTermPopup.tsx:339
#, c-format
msgid "Pay deadline"
-msgstr ""
+msgstr "Son ödeme tarihi"
#: src/components/ShowFullContractTermPopup.tsx:354
#, c-format
msgid "Fulfillment URL"
-msgstr ""
+msgstr "Gönderim URL'si"
#: src/components/ShowFullContractTermPopup.tsx:360
#, c-format
msgid "Fulfillment message"
-msgstr ""
+msgstr "Gönderim mesajı"
#: src/components/ShowFullContractTermPopup.tsx:370
#, c-format
msgid "Max deposit fee"
-msgstr ""
+msgstr "Azami para yatırma ücreti"
#: src/components/ShowFullContractTermPopup.tsx:378
#, c-format
msgid "Max fee"
-msgstr ""
+msgstr "Azami ücret"
#: src/components/ShowFullContractTermPopup.tsx:386
#, c-format
msgid "Minimum age"
-msgstr ""
+msgstr "Asgari yaş"
#: src/components/ShowFullContractTermPopup.tsx:398
#, c-format
msgid "Wire fee amortization"
-msgstr ""
+msgstr "Banka havalesi amortismanı"
#: src/components/ShowFullContractTermPopup.tsx:404
#, c-format
msgid "Auditors"
-msgstr ""
+msgstr "Denetçiler"
#: src/components/ShowFullContractTermPopup.tsx:419
-#, fuzzy, c-format
+#, c-format
msgid "Exchanges"
-msgstr "Exchange"
+msgstr "Borsalar"
#: src/components/Part.tsx:148
#, c-format
msgid "Bank account"
-msgstr ""
+msgstr "Banka hesabı"
#: src/components/Part.tsx:160
#, c-format
msgid "Bitcoin address"
-msgstr ""
+msgstr "Bitcoin adresi"
#: src/components/Part.tsx:163
#, c-format
msgid "IBAN"
-msgstr ""
+msgstr "IBAN"
#: src/cta/Deposit/views.tsx:38
-#, fuzzy, c-format
+#, c-format
msgid "Could not load deposit status"
-msgstr "Bakiye sayfası yüklenemedi"
+msgstr "Para yatırma durumu yüklenemedi"
#: src/cta/Deposit/views.tsx:52
#, c-format
msgid "Digital cash deposit"
-msgstr ""
+msgstr "Dijital para yatırma"
#: src/cta/Deposit/views.tsx:58
#, c-format
msgid "Cost"
-msgstr ""
+msgstr "Maliyet"
#: src/cta/Deposit/views.tsx:66
#, c-format
@@ -478,17 +480,17 @@ msgstr "Ücretler"
#: src/cta/Deposit/views.tsx:73
#, c-format
msgid "To be received"
-msgstr ""
+msgstr "Alınacak"
#: src/cta/Deposit/views.tsx:84
#, c-format
msgid "Send &nbsp; %1$s"
-msgstr ""
+msgstr "&nbsp;%1$s gönder"
#: src/components/BankDetailsByPaytoType.tsx:63
#, c-format
msgid "Bitcoin transfer details"
-msgstr ""
+msgstr "Bitcoin transfer ayrıntıları"
#: src/components/BankDetailsByPaytoType.tsx:66
#, c-format
@@ -515,7 +517,7 @@ msgstr ""
#: src/components/BankDetailsByPaytoType.tsx:110
#, c-format
msgid "Account"
-msgstr ""
+msgstr "Hesap"
#: src/components/BankDetailsByPaytoType.tsx:116
#, c-format
@@ -530,7 +532,7 @@ msgstr ""
#: src/components/BankDetailsByPaytoType.tsx:148
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Konu"
#: src/components/BankDetailsByPaytoType.tsx:154
#, c-format
@@ -555,7 +557,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:209
#, c-format
msgid "Send"
-msgstr ""
+msgstr "Göder"
#: src/wallet/Transaction.tsx:216
#, c-format
@@ -565,12 +567,12 @@ msgstr "Yeniden deneyin"
#: src/wallet/Transaction.tsx:224
#, c-format
msgid "Forget"
-msgstr ""
+msgstr "Unut"
#: src/wallet/Transaction.tsx:241
#, c-format
msgid "Caution!"
-msgstr ""
+msgstr "Dikkat!"
#: src/wallet/Transaction.tsx:244
#, c-format
@@ -613,7 +615,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:325
#, c-format
msgid "Details"
-msgstr ""
+msgstr "Detaylar"
#: src/wallet/Transaction.tsx:360
#, fuzzy, c-format
@@ -639,7 +641,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:420
#, c-format
msgid "Offer"
-msgstr ""
+msgstr "Teklif"
#: src/wallet/Transaction.tsx:431
#, fuzzy, c-format
@@ -649,17 +651,17 @@ msgstr "İkramiye kabul edildi"
#: src/wallet/Transaction.tsx:438
#, c-format
msgid "Merchant"
-msgstr ""
+msgstr "Satıcı"
#: src/wallet/Transaction.tsx:443
#, c-format
msgid "Invoice ID"
-msgstr ""
+msgstr "Fatura Kimliği"
#: src/wallet/Transaction.tsx:470
#, c-format
msgid "Deposit"
-msgstr ""
+msgstr "Yatır"
#: src/wallet/Transaction.tsx:496
#, fuzzy, c-format
@@ -669,7 +671,7 @@ msgstr "Ücreti yenile"
#: src/wallet/Transaction.tsx:517
#, c-format
msgid "Tip"
-msgstr ""
+msgstr "Bahşiş"
#: src/wallet/Transaction.tsx:542
#, c-format
@@ -689,27 +691,27 @@ msgstr ""
#: src/wallet/Transaction.tsx:593
#, c-format
msgid "copy"
-msgstr ""
+msgstr "kopyala"
#: src/wallet/Transaction.tsx:596
#, c-format
msgid "hide qr"
-msgstr ""
+msgstr "qr'yi gizle"
#: src/wallet/Transaction.tsx:608
#, c-format
msgid "show qr"
-msgstr ""
+msgstr "qr'yi göster"
#: src/wallet/Transaction.tsx:620
#, c-format
msgid "Credit"
-msgstr ""
+msgstr "Kredi"
#: src/wallet/Transaction.tsx:624
#, c-format
msgid "Invoice"
-msgstr ""
+msgstr "Fatura"
#: src/wallet/Transaction.tsx:635
#, c-format
@@ -719,7 +721,7 @@ msgstr "Exchange"
#: src/wallet/Transaction.tsx:641
#, c-format
msgid "URI"
-msgstr ""
+msgstr "URI"
#: src/wallet/Transaction.tsx:667
#, c-format
@@ -729,52 +731,52 @@ msgstr ""
#: src/wallet/Transaction.tsx:710
#, c-format
msgid "Transfer"
-msgstr ""
+msgstr "Transfer"
#: src/wallet/Transaction.tsx:844
#, c-format
msgid "Country"
-msgstr ""
+msgstr "Ülke"
#: src/wallet/Transaction.tsx:852
#, c-format
msgid "Address lines"
-msgstr ""
+msgstr "Adres satırı"
#: src/wallet/Transaction.tsx:860
#, c-format
msgid "Building number"
-msgstr ""
+msgstr "Bina numarası"
#: src/wallet/Transaction.tsx:868
#, c-format
msgid "Building name"
-msgstr ""
+msgstr "Bina adı"
#: src/wallet/Transaction.tsx:876
#, c-format
msgid "Street"
-msgstr ""
+msgstr "Sokak"
#: src/wallet/Transaction.tsx:884
#, c-format
msgid "Post code"
-msgstr ""
+msgstr "Posta kodu"
#: src/wallet/Transaction.tsx:892
#, c-format
msgid "Town location"
-msgstr ""
+msgstr "Kasaba konumu"
#: src/wallet/Transaction.tsx:900
#, c-format
msgid "Town"
-msgstr ""
+msgstr "Kasaba"
#: src/wallet/Transaction.tsx:908
#, c-format
msgid "District"
-msgstr ""
+msgstr "Semt"
#: src/wallet/Transaction.tsx:916
#, c-format
@@ -784,17 +786,17 @@ msgstr ""
#: src/wallet/Transaction.tsx:935
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Tarih"
#: src/wallet/Transaction.tsx:990
#, c-format
msgid "Transaction fees"
-msgstr ""
+msgstr "İşlem ücretleri"
#: src/wallet/Transaction.tsx:1004
#, c-format
msgid "Total"
-msgstr ""
+msgstr "Toplam"
#: src/wallet/Transaction.tsx:1074
#, c-format
@@ -804,17 +806,17 @@ msgstr "Para çek"
#: src/wallet/Transaction.tsx:1146
#, c-format
msgid "Price"
-msgstr ""
+msgstr "Fiyat"
#: src/wallet/Transaction.tsx:1156
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "İade edildi"
#: src/wallet/Transaction.tsx:1220
#, c-format
msgid "Delivery"
-msgstr ""
+msgstr "Teslimat"
#: src/wallet/Transaction.tsx:1335
#, fuzzy, c-format
@@ -824,77 +826,77 @@ msgstr "Çekildi"
#: src/cta/Payment/views.tsx:57
#, c-format
msgid "Could not load pay status"
-msgstr ""
+msgstr "Ödeme durumu yüklenemedi"
#: src/cta/Payment/views.tsx:87
#, c-format
msgid "Digital cash payment"
-msgstr ""
+msgstr "Dijital ödeme"
#: src/cta/Payment/views.tsx:119
#, c-format
msgid "Purchase"
-msgstr ""
+msgstr "Satın al"
#: src/cta/Payment/views.tsx:149
#, c-format
msgid "Receipt"
-msgstr ""
+msgstr "Fiş"
#: src/cta/Payment/views.tsx:156
#, c-format
msgid "Valid until"
-msgstr ""
+msgstr "Geçerlilik"
#: src/cta/Payment/views.tsx:191
#, c-format
msgid "List of products"
-msgstr ""
+msgstr "Ürün listesi"
#: src/cta/Payment/views.tsx:242
#, c-format
msgid "free"
-msgstr ""
+msgstr "ücretsiz"
#: src/cta/Payment/views.tsx:263
#, c-format
msgid "Already paid, you are going to be redirected to %1$s"
-msgstr ""
+msgstr "Zaten ödeme yaptınız, %1$s adresine yönlendirileceksiniz"
#: src/cta/Payment/views.tsx:274
#, c-format
msgid "Already paid"
-msgstr ""
+msgstr "Zaten ödendi"
#: src/cta/Payment/views.tsx:280
#, c-format
msgid "Already claimed"
-msgstr ""
+msgstr "Zaten talep edilmiş"
#: src/cta/Payment/views.tsx:296
#, c-format
msgid "Pay with a mobile phone"
-msgstr ""
+msgstr "Cep telefonuyla ödeme yapın"
#: src/cta/Payment/views.tsx:298
#, c-format
msgid "Hide QR"
-msgstr ""
+msgstr "QR'yi gizle"
#: src/cta/Payment/views.tsx:305
#, c-format
msgid "Scan the QR code or &nbsp; %1$s"
-msgstr ""
+msgstr "QR kodunu tarayın veya&nbsp;%1$s"
#: src/cta/Payment/views.tsx:346
#, c-format
msgid "Pay &nbsp; %1$s"
-msgstr ""
+msgstr "%1$snbsp;öde"
#: src/cta/Payment/views.tsx:360
#, c-format
msgid "You have no balance for this currency. Withdraw digital cash first."
-msgstr ""
+msgstr "Bu para birimi için bakiyeniz yok. Önce dijital parayı çekin."
#: src/cta/Payment/views.tsx:364
#, c-format
@@ -902,6 +904,8 @@ msgid ""
"Could not find enough coins to pay. Even if you have enough %1$s some "
"restriction may apply."
msgstr ""
+"Ödemeye yetecek kadar para bulunamadı. Yeterli %1$s olsa bile bazı "
+"kısıtlamalar geçerli olabilir."
#: src/cta/Payment/views.tsx:366
#, fuzzy, c-format
@@ -911,7 +915,7 @@ msgstr "Gösterecek bakiyeniz yok."
#: src/cta/Payment/views.tsx:395
#, c-format
msgid "Merchant message"
-msgstr ""
+msgstr "Satıcı mesajı"
#: src/cta/Refund/views.tsx:34
#, fuzzy, c-format
@@ -1286,7 +1290,7 @@ msgstr ""
#: src/wallet/CreateManualWithdraw.tsx:277
#, c-format
msgid "Start withdrawal"
-msgstr ""
+msgstr "Para çekme işlemini başlat"
#: src/wallet/DepositPage/views.tsx:38
#, fuzzy, c-format
diff --git a/packages/taler-wallet-webextension/src/i18n/uk.po b/packages/taler-wallet-webextension/src/i18n/uk.po
index c4f5d6537..caee8330e 100644
--- a/packages/taler-wallet-webextension/src/i18n/uk.po
+++ b/packages/taler-wallet-webextension/src/i18n/uk.po
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-03-05 13:03+0000\n"
-"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"PO-Revision-Date: 2024-08-07 10:40+0000\n"
+"Last-Translator: Vlada Svirsh <vlada.svirsh@students.bfh.ch>\n"
"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/uk/>\n"
"Language: uk\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.5.5\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -28,12 +28,12 @@ msgstr "Баланс"
#: src/NavigationBar.tsx:142
#, c-format
msgid "Backup"
-msgstr "Бекап"
+msgstr "Резервна копія"
#: src/NavigationBar.tsx:147
#, c-format
msgid "QR Reader and Taler URI"
-msgstr "QR-читалка та Taler URI"
+msgstr "Зчитувач QR та Taler URI"
#: src/NavigationBar.tsx:154
#, c-format
@@ -46,14 +46,14 @@ msgid "Dev"
msgstr "Розробка"
#: src/mui/Typography.tsx:122
-#, c-format, fuzzy
+#, c-format
msgid "%1$s"
msgstr "%1$s"
#: src/components/PendingTransactions.tsx:74
#, c-format
msgid "PENDING OPERATIONS"
-msgstr "НЕЗАВЕРШЕНІ ОПЕРАЦІЇ"
+msgstr "ОЧІКУВАНІ ОПЕРАЦІЇ"
#: src/components/Loading.tsx:36
#, c-format
@@ -177,7 +177,7 @@ msgstr "Видалити зберігача"
#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
msgid "This provider has reported an error"
-msgstr "Цей постачальник повідомив про помилку"
+msgstr "Цей зберігач повідомив про помилку"
#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
@@ -207,32 +207,32 @@ msgstr "Резервна копія дійсна до"
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr "Відмінити"
+msgstr "Скасувати"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
msgid "Open reserve page"
-msgstr "Показати резерв"
+msgstr "Відкрити резервну сторінку"
#: src/wallet/AddNewActionView.tsx:70
#, c-format
msgid "Open pay page"
-msgstr "Показати сторінку оплати"
+msgstr "Відкрити сторінку оплати"
#: src/wallet/AddNewActionView.tsx:72
#, c-format
msgid "Open refund page"
-msgstr "Показати відшкодування"
+msgstr "Відкрити сторінку повернень"
#: src/wallet/AddNewActionView.tsx:74
#, c-format
msgid "Open tip page"
-msgstr "Показати чайові"
+msgstr "Відкрити сторінку чайових"
#: src/wallet/AddNewActionView.tsx:76
#, c-format
msgid "Open withdraw page"
-msgstr "Показати списання"
+msgstr "Відкрити сторінку зняття"
#: src/popup/NoBalanceHelp.tsx:43
#, c-format
@@ -242,7 +242,7 @@ msgstr "Отримати е-готівку"
#: src/popup/BalancePage.tsx:138
#, c-format
msgid "Could not load balance page"
-msgstr "Не вдалося показати залишок"
+msgstr "Не вдалося завантажити сторінку балансу"
#: src/popup/BalancePage.tsx:175
#, c-format
@@ -262,227 +262,227 @@ msgstr "Taler Дія"
#: src/popup/TalerActionFound.tsx:49
#, c-format
msgid "This page has pay action."
-msgstr ""
+msgstr "Ця сторінка має дію оплати."
#: src/popup/TalerActionFound.tsx:63
#, c-format
msgid "This page has a withdrawal action."
-msgstr ""
+msgstr "Ця сторінка має дію зняття коштів."
#: src/popup/TalerActionFound.tsx:79
#, c-format
msgid "This page has a tip action."
-msgstr ""
+msgstr "Ця сторінка має дію чайових."
#: src/popup/TalerActionFound.tsx:93
#, c-format
msgid "This page has a notify reserve action."
-msgstr ""
+msgstr "Ця сторінка має дію сповіщення про резерв."
#: src/popup/TalerActionFound.tsx:102
#, c-format
msgid "Notify"
-msgstr ""
+msgstr "Сповістити"
#: src/popup/TalerActionFound.tsx:109
#, c-format
msgid "This page has a refund action."
-msgstr ""
+msgstr "Ця сторінка має дію повернення коштів."
#: src/popup/TalerActionFound.tsx:123
#, c-format
msgid "This page has a malformed taler uri."
-msgstr ""
+msgstr "Ця сторінка має некоректний URI Taler."
#: src/popup/TalerActionFound.tsx:134
#, c-format
msgid "Dismiss"
-msgstr ""
+msgstr "Закрити"
#: src/popup/Application.tsx:177
#, c-format
msgid "this popup is being closed and you are being redirected to %1$s"
-msgstr ""
+msgstr "це спливаюче вікно закривається, і ви перенаправляєтеся на %1$s"
#: src/components/ShowFullContractTermPopup.tsx:158
#, c-format
msgid "Could not load purchase proposal details"
-msgstr ""
+msgstr "Не вдалося завантажити деталі пропозиції покупки"
#: src/components/ShowFullContractTermPopup.tsx:183
#, c-format
msgid "Order Id"
-msgstr ""
+msgstr "Номер замовлення"
#: src/components/ShowFullContractTermPopup.tsx:189
#, c-format
msgid "Summary"
-msgstr ""
+msgstr "Підсумок"
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Сума"
#: src/components/ShowFullContractTermPopup.tsx:203
#, c-format
msgid "Merchant name"
-msgstr ""
+msgstr "Ім'я продавця"
#: src/components/ShowFullContractTermPopup.tsx:209
#, c-format
msgid "Merchant jurisdiction"
-msgstr ""
+msgstr "Юрисдикція продавця"
#: src/components/ShowFullContractTermPopup.tsx:215
#, c-format
msgid "Merchant address"
-msgstr ""
+msgstr "Адреса продавця"
#: src/components/ShowFullContractTermPopup.tsx:221
#, c-format
msgid "Merchant logo"
-msgstr ""
+msgstr "Логотип продавця"
#: src/components/ShowFullContractTermPopup.tsx:234
#, c-format
msgid "Merchant website"
-msgstr ""
+msgstr "Вебсайт продавця"
#: src/components/ShowFullContractTermPopup.tsx:240
#, c-format
msgid "Merchant email"
-msgstr ""
+msgstr "Електронна пошта продавця"
#: src/components/ShowFullContractTermPopup.tsx:246
#, c-format
msgid "Merchant public key"
-msgstr ""
+msgstr "Публічний ключ продавця"
#: src/components/ShowFullContractTermPopup.tsx:256
#, c-format
msgid "Delivery date"
-msgstr ""
+msgstr "Дата доставки"
#: src/components/ShowFullContractTermPopup.tsx:271
#, c-format
msgid "Delivery location"
-msgstr ""
+msgstr "Місце доставки"
#: src/components/ShowFullContractTermPopup.tsx:277
#, c-format
msgid "Products"
-msgstr ""
+msgstr "Товари"
#: src/components/ShowFullContractTermPopup.tsx:289
#, c-format
msgid "Created at"
-msgstr ""
+msgstr "Створено о"
#: src/components/ShowFullContractTermPopup.tsx:304
#, c-format
msgid "Refund deadline"
-msgstr ""
+msgstr "Термін повернення"
#: src/components/ShowFullContractTermPopup.tsx:319
#, c-format
msgid "Auto refund"
-msgstr ""
+msgstr "Автоматичне повернення"
#: src/components/ShowFullContractTermPopup.tsx:339
#, c-format
msgid "Pay deadline"
-msgstr ""
+msgstr "Термін оплати"
#: src/components/ShowFullContractTermPopup.tsx:354
#, c-format
msgid "Fulfillment URL"
-msgstr ""
+msgstr "URL виконання"
#: src/components/ShowFullContractTermPopup.tsx:360
#, c-format
msgid "Fulfillment message"
-msgstr ""
+msgstr "Повідомлення про виконання"
#: src/components/ShowFullContractTermPopup.tsx:370
#, c-format
msgid "Max deposit fee"
-msgstr ""
+msgstr "Максимальна комісія за депозит"
#: src/components/ShowFullContractTermPopup.tsx:378
#, c-format
msgid "Max fee"
-msgstr ""
+msgstr "Максимальна комісія"
#: src/components/ShowFullContractTermPopup.tsx:386
#, c-format
msgid "Minimum age"
-msgstr ""
+msgstr "Мінімальний вік"
#: src/components/ShowFullContractTermPopup.tsx:398
#, c-format
msgid "Wire fee amortization"
-msgstr ""
+msgstr "Амортизація комісії за переказ"
#: src/components/ShowFullContractTermPopup.tsx:404
#, c-format
msgid "Auditors"
-msgstr ""
+msgstr "Аудитори"
#: src/components/ShowFullContractTermPopup.tsx:419
#, c-format
msgid "Exchanges"
-msgstr ""
+msgstr "Обмінники"
#: src/components/Part.tsx:148
#, c-format
msgid "Bank account"
-msgstr ""
+msgstr "Банківський рахунок"
#: src/components/Part.tsx:160
#, c-format
msgid "Bitcoin address"
-msgstr ""
+msgstr "Адреса Bitcoin"
#: src/components/Part.tsx:163
#, c-format
msgid "IBAN"
-msgstr ""
+msgstr "IBAN"
#: src/cta/Deposit/views.tsx:38
#, c-format
msgid "Could not load deposit status"
-msgstr ""
+msgstr "Не вдалося завантажити статус депозиту"
#: src/cta/Deposit/views.tsx:52
#, c-format
msgid "Digital cash deposit"
-msgstr ""
+msgstr "Депозит електронних грошей"
#: src/cta/Deposit/views.tsx:58
#, c-format
msgid "Cost"
-msgstr ""
+msgstr "Вартість"
#: src/cta/Deposit/views.tsx:66
#, c-format
msgid "Fee"
-msgstr ""
+msgstr "Комісія"
#: src/cta/Deposit/views.tsx:73
#, c-format
msgid "To be received"
-msgstr ""
+msgstr "До отримання"
#: src/cta/Deposit/views.tsx:84
#, c-format
msgid "Send &nbsp; %1$s"
-msgstr ""
+msgstr "Надіслати &nbsp; %1$s"
#: src/components/BankDetailsByPaytoType.tsx:63
#, c-format
msgid "Bitcoin transfer details"
-msgstr ""
+msgstr "Деталі переказу Bitcoin"
#: src/components/BankDetailsByPaytoType.tsx:66
#, c-format
@@ -491,6 +491,8 @@ msgid ""
"account and the other two are segwit fake address for metadata with an minimum "
"amount."
msgstr ""
+"Для обміну потрібна транзакція з 3 виходами: один вихід - це рахунок обміну, "
+"а два інші - segwit вигадані адреси для метаданих з мінімальною сумою."
#: src/components/BankDetailsByPaytoType.tsx:74
#, c-format
@@ -498,71 +500,75 @@ msgid ""
"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
"recipient and copy addresses and amounts"
msgstr ""
+"У гаманці bitcoincore використовуйте кнопку &apos;Додати отримувача&apos;, "
+"щоб додати двох додаткових отримувачів і скопіювати адреси та суми"
#: src/components/BankDetailsByPaytoType.tsx:98
#, c-format
msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
msgstr ""
+"Переконайтеся, що сума показує %1$s BTC, інакше вам потрібно змінити базову "
+"одиницю на BTC"
#: src/components/BankDetailsByPaytoType.tsx:110
#, c-format
msgid "Account"
-msgstr ""
+msgstr "Рахунок"
#: src/components/BankDetailsByPaytoType.tsx:116
#, c-format
msgid "Bank host"
-msgstr ""
+msgstr "Банк-хост"
#: src/components/BankDetailsByPaytoType.tsx:139
#, c-format
msgid "Bank transfer details"
-msgstr ""
+msgstr "Деталі банківського переказу"
#: src/components/BankDetailsByPaytoType.tsx:148
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Призначення"
#: src/components/BankDetailsByPaytoType.tsx:154
#, c-format
msgid "Receiver name"
-msgstr ""
+msgstr "Ім'я отримувача"
#: src/wallet/Transaction.tsx:98
#, c-format
msgid "Could not load the transaction information"
-msgstr ""
+msgstr "Не вдалося завантажити інформацію про транзакцію"
#: src/wallet/Transaction.tsx:191
#, c-format
msgid "There was an error trying to complete the transaction"
-msgstr ""
+msgstr "Сталася помилка під час спроби завершити транзакцію"
#: src/wallet/Transaction.tsx:200
#, c-format
msgid "This transaction is not completed"
-msgstr ""
+msgstr "Ця транзакція не завершена"
#: src/wallet/Transaction.tsx:209
#, c-format
msgid "Send"
-msgstr ""
+msgstr "Надіслати"
#: src/wallet/Transaction.tsx:216
#, c-format
msgid "Retry"
-msgstr ""
+msgstr "Спробувати ще раз"
#: src/wallet/Transaction.tsx:224
#, c-format
msgid "Forget"
-msgstr ""
+msgstr "Забути"
#: src/wallet/Transaction.tsx:241
#, c-format
msgid "Caution!"
-msgstr ""
+msgstr "Увага!"
#: src/wallet/Transaction.tsx:244
#, c-format
@@ -570,16 +576,18 @@ msgid ""
"If you have already wired money to the exchange you will loose the chance to get "
"the coins form it."
msgstr ""
+"Якщо Ви вже переказали гроші на обмінник, Ви втратите можливість отримати з "
+"нього монети."
#: src/wallet/Transaction.tsx:259
#, c-format
msgid "Confirm"
-msgstr ""
+msgstr "Підтвердити"
#: src/wallet/Transaction.tsx:267
#, c-format
msgid "Withdrawal"
-msgstr ""
+msgstr "Зняття"
#: src/wallet/Transaction.tsx:286
#, c-format
@@ -587,6 +595,8 @@ msgid ""
"Make sure to use the correct subject, otherwise the money will not arrive in "
"this wallet."
msgstr ""
+"Переконайтеся, що використовуєте правильне призначення, інакше гроші не "
+"надійдуть на цей гаманець."
#: src/wallet/Transaction.tsx:298
#, c-format
@@ -594,296 +604,301 @@ msgid ""
"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
"there is no pending step."
msgstr ""
+"Банк ще не підтвердив банківський переказ. Перейдіть до %1$s %2$s і "
+"переконайтеся, що немає жодного незавершеного кроку."
#: src/wallet/Transaction.tsx:316
#, c-format
msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
msgstr ""
+"Банк підтвердив банківський переказ. Очікуємо, що обмінник надішле монети"
#: src/wallet/Transaction.tsx:325
#, c-format
msgid "Details"
-msgstr ""
+msgstr "Деталі"
#: src/wallet/Transaction.tsx:360
#, c-format
msgid "Payment"
-msgstr ""
+msgstr "Оплата"
#: src/wallet/Transaction.tsx:378
#, c-format
msgid "Refunds"
-msgstr ""
+msgstr "Повернення"
#: src/wallet/Transaction.tsx:385
#, c-format
msgid "%1$s %2$s on %3$s"
-msgstr ""
+msgstr "%1$s %2$s на %3$s"
#: src/wallet/Transaction.tsx:415
#, c-format
msgid "Merchant created a refund for this order but was not automatically picked up."
msgstr ""
+"Продавець створив повернення для цього замовлення, але воно не було "
+"автоматично оброблене."
#: src/wallet/Transaction.tsx:420
#, c-format
msgid "Offer"
-msgstr ""
+msgstr "Пропозиція"
#: src/wallet/Transaction.tsx:431
#, c-format
msgid "Accept"
-msgstr ""
+msgstr "Прийняти"
#: src/wallet/Transaction.tsx:438
#, c-format
msgid "Merchant"
-msgstr ""
+msgstr "Продавець"
#: src/wallet/Transaction.tsx:443
#, c-format
msgid "Invoice ID"
-msgstr ""
+msgstr "№ рахунку-фактури"
#: src/wallet/Transaction.tsx:470
#, c-format
msgid "Deposit"
-msgstr ""
+msgstr "Депозит"
#: src/wallet/Transaction.tsx:496
#, c-format
msgid "Refresh"
-msgstr ""
+msgstr "Оновити"
#: src/wallet/Transaction.tsx:517
#, c-format
msgid "Tip"
-msgstr ""
+msgstr "Чайові"
#: src/wallet/Transaction.tsx:542
#, c-format
msgid "Refund"
-msgstr ""
+msgstr "Повернення"
#: src/wallet/Transaction.tsx:555
#, c-format
msgid "Original order ID"
-msgstr ""
+msgstr "Початковий № замовлення"
#: src/wallet/Transaction.tsx:568
#, c-format
msgid "Purchase summary"
-msgstr ""
+msgstr "Інформація про покупку"
#: src/wallet/Transaction.tsx:593
#, c-format
msgid "copy"
-msgstr ""
+msgstr "копіювати"
#: src/wallet/Transaction.tsx:596
#, c-format
msgid "hide qr"
-msgstr ""
+msgstr "сховати qr"
#: src/wallet/Transaction.tsx:608
#, c-format
msgid "show qr"
-msgstr ""
+msgstr "показати qr"
#: src/wallet/Transaction.tsx:620
#, c-format
msgid "Credit"
-msgstr ""
+msgstr "Кредит"
#: src/wallet/Transaction.tsx:624
#, c-format
msgid "Invoice"
-msgstr ""
+msgstr "Рахунок-фактура"
#: src/wallet/Transaction.tsx:635
#, c-format
msgid "Exchange"
-msgstr ""
+msgstr "Exchange"
#: src/wallet/Transaction.tsx:641
#, c-format
msgid "URI"
-msgstr ""
+msgstr "URI"
#: src/wallet/Transaction.tsx:667
#, c-format
msgid "Debit"
-msgstr ""
+msgstr "Дебет"
#: src/wallet/Transaction.tsx:710
#, c-format
msgid "Transfer"
-msgstr ""
+msgstr "Переказати"
#: src/wallet/Transaction.tsx:844
#, c-format
msgid "Country"
-msgstr ""
+msgstr "Країна"
#: src/wallet/Transaction.tsx:852
#, c-format
msgid "Address lines"
-msgstr ""
+msgstr "Рядки адреси"
#: src/wallet/Transaction.tsx:860
#, c-format
msgid "Building number"
-msgstr ""
+msgstr "Номер будинку"
#: src/wallet/Transaction.tsx:868
#, c-format
msgid "Building name"
-msgstr ""
+msgstr "Назва будинку"
#: src/wallet/Transaction.tsx:876
#, c-format
msgid "Street"
-msgstr ""
+msgstr "Вулиця"
#: src/wallet/Transaction.tsx:884
#, c-format
msgid "Post code"
-msgstr ""
+msgstr "Поштовий індекс"
#: src/wallet/Transaction.tsx:892
#, c-format
msgid "Town location"
-msgstr ""
+msgstr "Область міста"
#: src/wallet/Transaction.tsx:900
#, c-format
msgid "Town"
-msgstr ""
+msgstr "Місто"
#: src/wallet/Transaction.tsx:908
#, c-format
msgid "District"
-msgstr ""
+msgstr "Район"
#: src/wallet/Transaction.tsx:916
#, c-format
msgid "Country subdivision"
-msgstr ""
+msgstr "Регіон країни"
#: src/wallet/Transaction.tsx:935
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Дата"
#: src/wallet/Transaction.tsx:990
#, c-format
msgid "Transaction fees"
-msgstr ""
+msgstr "Комісії за транзакцію"
#: src/wallet/Transaction.tsx:1004
#, c-format
msgid "Total"
-msgstr ""
+msgstr "Загальна сума"
#: src/wallet/Transaction.tsx:1074
#, c-format
msgid "Withdraw"
-msgstr ""
+msgstr "Зняття коштів"
#: src/wallet/Transaction.tsx:1146
#, c-format
msgid "Price"
-msgstr ""
+msgstr "Ціна"
#: src/wallet/Transaction.tsx:1156
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "Повернено"
#: src/wallet/Transaction.tsx:1220
#, c-format
msgid "Delivery"
-msgstr ""
+msgstr "Доставка"
#: src/wallet/Transaction.tsx:1335
#, c-format
msgid "Total transfer"
-msgstr ""
+msgstr "Загальний переказ"
#: src/cta/Payment/views.tsx:57
#, c-format
msgid "Could not load pay status"
-msgstr ""
+msgstr "Не вдалося завантажити статус оплати"
#: src/cta/Payment/views.tsx:87
#, c-format
msgid "Digital cash payment"
-msgstr ""
+msgstr "Оплата електронними грошима"
#: src/cta/Payment/views.tsx:119
#, c-format
msgid "Purchase"
-msgstr ""
+msgstr "Покупка"
#: src/cta/Payment/views.tsx:149
#, c-format
msgid "Receipt"
-msgstr ""
+msgstr "Чек"
#: src/cta/Payment/views.tsx:156
#, c-format
msgid "Valid until"
-msgstr ""
+msgstr "Дійсний до"
#: src/cta/Payment/views.tsx:191
#, c-format
msgid "List of products"
-msgstr ""
+msgstr "Список продуктів"
#: src/cta/Payment/views.tsx:242
#, c-format
msgid "free"
-msgstr ""
+msgstr "безкоштовно"
#: src/cta/Payment/views.tsx:263
#, c-format
msgid "Already paid, you are going to be redirected to %1$s"
-msgstr ""
+msgstr "Вже оплачено, Вас буде перенаправлено на %1$s"
#: src/cta/Payment/views.tsx:274
#, c-format
msgid "Already paid"
-msgstr ""
+msgstr "Вже оплачено"
#: src/cta/Payment/views.tsx:280
#, c-format
msgid "Already claimed"
-msgstr ""
+msgstr "Вже отримано"
#: src/cta/Payment/views.tsx:296
#, c-format
msgid "Pay with a mobile phone"
-msgstr ""
+msgstr "Оплатити мобільним телефоном"
#: src/cta/Payment/views.tsx:298
#, c-format
msgid "Hide QR"
-msgstr ""
+msgstr "Сховати QR"
#: src/cta/Payment/views.tsx:305
#, c-format
msgid "Scan the QR code or &nbsp; %1$s"
-msgstr ""
+msgstr "Скануйте QR-код або &nbsp; %1$s"
#: src/cta/Payment/views.tsx:346
#, c-format
msgid "Pay &nbsp; %1$s"
-msgstr ""
+msgstr "Оплатити &nbsp; %1$s"
#: src/cta/Payment/views.tsx:360
#, c-format
msgid "You have no balance for this currency. Withdraw digital cash first."
-msgstr ""
+msgstr "У вас немає балансу в цій валюті. Спочатку зніміть електронні гроші."
#: src/cta/Payment/views.tsx:364
#, c-format
@@ -891,231 +906,235 @@ msgid ""
"Could not find enough coins to pay. Even if you have enough %1$s some "
"restriction may apply."
msgstr ""
+"Не вдалося знайти достатню кількість монет для оплати. Навіть якщо у вас "
+"достатньо %1$s, можуть застосовуватися певні обмеження."
#: src/cta/Payment/views.tsx:366
#, c-format
msgid "Your current balance is not enough."
-msgstr ""
+msgstr "Ваш поточний баланс недостатній."
#: src/cta/Payment/views.tsx:395
#, c-format
msgid "Merchant message"
-msgstr ""
+msgstr "Повідомлення продавця"
#: src/cta/Refund/views.tsx:34
#, c-format
msgid "Could not load refund status"
-msgstr ""
+msgstr "Не вдалося завантажити статус повернення"
#: src/cta/Refund/views.tsx:48
#, c-format
msgid "Digital cash refund"
-msgstr ""
+msgstr "Повернення електронних грошей"
#: src/cta/Refund/views.tsx:52
#, c-format
msgid "You&apos;ve ignored the tip."
-msgstr ""
+msgstr "Ви проігнорували чайові."
#: src/cta/Refund/views.tsx:70
#, c-format
msgid "The refund is in progress."
-msgstr ""
+msgstr "Повернення коштів в процесі."
#: src/cta/Refund/views.tsx:76
#, c-format
msgid "Total to refund"
-msgstr ""
+msgstr "Загальна сума до повернення"
#: src/cta/Refund/views.tsx:106
#, c-format
msgid "The merchant &quot;%1$s&quot; is offering you a refund."
-msgstr ""
+msgstr "Продавець &quot;%1$s&quot; пропонує вам повернення коштів."
#: src/cta/Refund/views.tsx:115
#, c-format
msgid "Order amount"
-msgstr ""
+msgstr "Сума замовлення"
#: src/cta/Refund/views.tsx:122
#, c-format
msgid "Already refunded"
-msgstr ""
+msgstr "Вже повернено"
#: src/cta/Refund/views.tsx:129
#, c-format
msgid "Refund offered"
-msgstr ""
+msgstr "Повернення запропоновано"
#: src/cta/Refund/views.tsx:145
#, c-format
msgid "Accept &nbsp; %1$s"
-msgstr ""
+msgstr "Прийняти &nbsp; %1$s"
#: src/cta/Tip/views.tsx:32
#, c-format
msgid "Could not load tip status"
-msgstr ""
+msgstr "Не вдалося завантажити статус чайових"
#: src/cta/Tip/views.tsx:45
#, c-format
msgid "Digital cash tip"
-msgstr ""
+msgstr "Електронні гроші за чайові"
#: src/cta/Tip/views.tsx:66
#, c-format
msgid "The merchant is offering you a tip"
-msgstr ""
+msgstr "Продавець пропонує вам чайові"
#: src/cta/Tip/views.tsx:74
#, c-format
msgid "Merchant URL"
-msgstr ""
+msgstr "URL продавця"
#: src/cta/Tip/views.tsx:90
#, c-format
msgid "Receive &nbsp; %1$s"
-msgstr ""
+msgstr "Отримати &nbsp; %1$s"
#: src/cta/Tip/views.tsx:114
#, c-format
msgid "Tip from %1$s accepted. Check your transactions list for more details."
msgstr ""
+"Чайові від %1$s прийнято. Перевірте свій список транзакцій для отримання "
+"додаткової інформації."
#: src/components/SelectList.tsx:66
#, c-format
msgid "Select one option"
-msgstr ""
+msgstr "Виберіть один варіант"
#: src/components/TermsOfService/views.tsx:39
#, c-format
msgid "Could not load"
-msgstr ""
+msgstr "Не вдалося завантажити"
#: src/components/TermsOfService/views.tsx:73
#, c-format
msgid "Show terms of service"
-msgstr ""
+msgstr "Показати умови отримання послуг"
#: src/components/TermsOfService/views.tsx:81
#, c-format
msgid "I accept the exchange terms of service"
-msgstr ""
+msgstr "Я приймаю Умови надання послуг, цього обмінника"
#: src/components/TermsOfService/views.tsx:107
#, c-format
msgid "Exchange doesn&apos;t have terms of service"
-msgstr ""
+msgstr "Обмінник не має умов надання послуг"
#: src/components/TermsOfService/views.tsx:135
#, c-format
msgid "Review exchange terms of service"
-msgstr ""
+msgstr "Переглянути умови надання послуг обмінника"
#: src/components/TermsOfService/views.tsx:146
#, c-format
msgid "Review new version of terms of service"
-msgstr ""
+msgstr "Переглянути нову версію Умов надання послуг"
#: src/components/TermsOfService/views.tsx:170
#, c-format
msgid "The exchange reply with a empty terms of service"
-msgstr ""
+msgstr "Обмінник відповів порожніми умовами надання послуг"
#: src/components/TermsOfService/views.tsx:193
#, c-format
msgid "Download Terms of Service"
-msgstr ""
+msgstr "Завантажити Умови надання послуг"
#: src/components/TermsOfService/views.tsx:204
#, c-format
msgid "Hide terms of service"
-msgstr ""
+msgstr "Сховати Умови надання послуг"
#: src/wallet/ExchangeSelection/views.tsx:117
#, c-format
msgid "Could not load exchange fees"
-msgstr ""
+msgstr "Не вдалося завантажити комісії обмінника"
#: src/wallet/ExchangeSelection/views.tsx:131
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Закрити"
#: src/wallet/ExchangeSelection/views.tsx:160
#, c-format
msgid "could not find any exchange"
-msgstr ""
+msgstr "не вдалося знайти жодного обмінника"
#: src/wallet/ExchangeSelection/views.tsx:166
#, c-format
msgid "could not find any exchange for the currency %1$s"
-msgstr ""
+msgstr "не вдалося знайти жодного обмінника для валюти %1$s"
#: src/wallet/ExchangeSelection/views.tsx:186
#, c-format
msgid "Service fee description"
-msgstr ""
+msgstr "Опис комісії за послуги"
#: src/wallet/ExchangeSelection/views.tsx:201
#, c-format
msgid "Select %1$s exchange"
-msgstr ""
+msgstr "Вибрати обмінник %1$s"
#: src/wallet/ExchangeSelection/views.tsx:215
#, c-format
msgid "Reset"
-msgstr ""
+msgstr "Скинути"
#: src/wallet/ExchangeSelection/views.tsx:218
#, c-format
msgid "Use this exchange"
-msgstr ""
+msgstr "Використовувати цей обмінник"
#: src/wallet/ExchangeSelection/views.tsx:230
#, c-format
msgid "Doesn&apos;t have auditors"
-msgstr ""
+msgstr "Немає аудиторів"
#: src/wallet/ExchangeSelection/views.tsx:241
#, c-format
msgid "currency"
-msgstr ""
+msgstr "валюта"
#: src/wallet/ExchangeSelection/views.tsx:249
#, c-format
msgid "Operations"
-msgstr ""
+msgstr "Операції"
#: src/wallet/ExchangeSelection/views.tsx:252
#, c-format
msgid "Deposits"
-msgstr ""
+msgstr "Депозити"
#: src/wallet/ExchangeSelection/views.tsx:259
#, c-format
msgid "Denomination"
-msgstr ""
+msgstr "Номінал"
#: src/wallet/ExchangeSelection/views.tsx:265
#, c-format
msgid "Until"
-msgstr ""
+msgstr "До"
#: src/wallet/ExchangeSelection/views.tsx:274
#, c-format
msgid "Withdrawals"
-msgstr ""
+msgstr "Зняття коштів"
#: src/wallet/ExchangeSelection/views.tsx:423
#, c-format
msgid "Currency"
-msgstr ""
+msgstr "Валюта"
#: src/wallet/ExchangeSelection/views.tsx:433
#, c-format
msgid "Coin operations"
-msgstr ""
+msgstr "Операції з монетами"
#: src/wallet/ExchangeSelection/views.tsx:436
#, c-format
@@ -1124,11 +1143,14 @@ msgid ""
"valid for a period of time. The exchange will charge the indicated amount every "
"time a coin is used in such operation."
msgstr ""
+"Кожна операція в цьому розділі може відрізнятися за номіналом і дійсна "
+"протягом певного періоду часу. Обмінник стягуватиме зазначену суму кожного "
+"разу, коли монета використовується в такій операції."
#: src/wallet/ExchangeSelection/views.tsx:545
#, c-format
msgid "Transfer operations"
-msgstr ""
+msgstr "Операції переказів"
#: src/wallet/ExchangeSelection/views.tsx:548
#, c-format
@@ -1137,96 +1159,99 @@ msgid ""
"for a period of time. The exchange will charge the indicated amount every time a "
"transfer is made."
msgstr ""
+"Кожна операція в цьому розділі може відрізнятися за типом переказу і дійсна "
+"протягом певного періоду часу. Обмінник стягуватиме зазначену суму кожного "
+"разу, коли здійснюється переказ."
#: src/wallet/ExchangeSelection/views.tsx:563
#, c-format
msgid "Operation"
-msgstr ""
+msgstr "Операція"
#: src/wallet/ExchangeSelection/views.tsx:583
#, c-format
msgid "Wallet operations"
-msgstr ""
+msgstr "Операції гаманця"
#: src/wallet/ExchangeSelection/views.tsx:597
#, c-format
msgid "Feature"
-msgstr ""
+msgstr "Функція"
#: src/cta/Withdraw/views.tsx:47
#, c-format
msgid "Could not get the info from the URI"
-msgstr ""
+msgstr "Не вдалося отримати інформацію з URI"
#: src/cta/Withdraw/views.tsx:60
#, c-format
msgid "Could not get info of withdrawal"
-msgstr ""
+msgstr "Не вдалося отримати інформацію про зняття коштів"
#: src/cta/Withdraw/views.tsx:74
#, c-format
msgid "Digital cash withdrawal"
-msgstr ""
+msgstr "Зняття електронних грошей"
#: src/cta/Withdraw/views.tsx:79
#, c-format
msgid "Could not finish the withdrawal operation"
-msgstr ""
+msgstr "Не вдалося завершити операцію зняття коштів"
#: src/cta/Withdraw/views.tsx:127
#, c-format
msgid "Age restriction"
-msgstr ""
+msgstr "Обмеження за віком"
#: src/cta/Withdraw/views.tsx:145
#, c-format
msgid "Withdraw &nbsp; %1$s"
-msgstr ""
+msgstr "Зняти &nbsp; %1$s"
#: src/cta/Withdraw/views.tsx:179
#, c-format
msgid "Withdraw to a mobile phone"
-msgstr ""
+msgstr "Зняти на мобільний телефон"
#: src/cta/InvoiceCreate/views.tsx:65
#, c-format
msgid "Digital invoice"
-msgstr ""
+msgstr "Електронний рахунок-фактура"
#: src/cta/InvoiceCreate/views.tsx:69
#, c-format
msgid "Could not finish the invoice creation"
-msgstr ""
+msgstr "Не вдалося завершити створення рахунку"
#: src/cta/InvoiceCreate/views.tsx:130
#, c-format
msgid "Create"
-msgstr ""
+msgstr "Створити"
#: src/cta/InvoicePay/views.tsx:63
#, c-format
msgid "Could not finish the payment operation"
-msgstr ""
+msgstr "Не вдалося завершити операцію оплати"
#: src/cta/TransferCreate/views.tsx:55
#, c-format
msgid "Digital cash transfer"
-msgstr ""
+msgstr "Переказ електронних грошей"
#: src/cta/TransferCreate/views.tsx:59
#, c-format
msgid "Could not finish the transfer creation"
-msgstr ""
+msgstr "Не вдалося завершити створення переказу"
#: src/cta/TransferPickup/views.tsx:57
#, c-format
msgid "Could not finish the pickup operation"
-msgstr ""
+msgstr "Не вдалося завершити операцію отримання"
#: src/wallet/CreateManualWithdraw.tsx:149
#, c-format
msgid "Manual Withdrawal for %1$s"
-msgstr ""
+msgstr "Ручне зняття для %1$s"
#: src/wallet/CreateManualWithdraw.tsx:154
#, c-format
@@ -1235,271 +1260,277 @@ msgid ""
"the coins to this wallet after receiving a wire transfer with the correct "
"subject."
msgstr ""
+"Виберіть обмінник, з якого будуть зняті монети. Обмінник надішле монети в "
+"цей гаманець після отримання банківського переказу з правильним призначенням."
#: src/wallet/CreateManualWithdraw.tsx:162
#, c-format
msgid "No exchange found for %1$s"
-msgstr ""
+msgstr "Не знайдено обмінника для %1$s"
#: src/wallet/CreateManualWithdraw.tsx:170
#, c-format
msgid "Add Exchange"
-msgstr ""
+msgstr "Додати обмінник"
#: src/wallet/CreateManualWithdraw.tsx:192
#, c-format
msgid "No exchange configured"
-msgstr ""
+msgstr "Обмінник не налаштовано"
#: src/wallet/CreateManualWithdraw.tsx:210
#, c-format
msgid "Can&apos;t create the reserve"
-msgstr ""
+msgstr "Не вдається створити резерв"
#: src/wallet/CreateManualWithdraw.tsx:277
#, c-format
msgid "Start withdrawal"
-msgstr ""
+msgstr "Розпочати зняття коштів"
#: src/wallet/DepositPage/views.tsx:38
#, c-format
msgid "Could not load deposit balance"
-msgstr ""
+msgstr "Не вдалося завантажити баланс депозиту"
#: src/wallet/DepositPage/views.tsx:51
#, c-format
msgid "A currency or an amount should be indicated"
-msgstr ""
+msgstr "Необхідно вказати валюту або суму"
#: src/wallet/DepositPage/views.tsx:67
#, c-format
msgid "There is no enough balance to make a deposit for currency %1$s"
-msgstr ""
+msgstr "Недостатньо балансу для внесення депозиту у валюті %1$s"
#: src/wallet/DepositPage/views.tsx:117
#, c-format
msgid "Send %1$s to your account"
-msgstr ""
+msgstr "Надіслати %1$s на ваш рахунок"
#: src/wallet/DepositPage/views.tsx:121
#, c-format
msgid "There is no account to make a deposit for currency %1$s"
-msgstr ""
+msgstr "Немає рахунку для внесення депозиту у валюті %1$s"
#: src/wallet/DepositPage/views.tsx:127
#, c-format
msgid "Add account"
-msgstr ""
+msgstr "Додати рахунок"
#: src/wallet/DepositPage/views.tsx:151
#, c-format
msgid "Select account"
-msgstr ""
+msgstr "Вибрати рахунок"
#: src/wallet/DepositPage/views.tsx:163
#, c-format
msgid "Add another account"
-msgstr ""
+msgstr "Додати інший рахунок"
#: src/wallet/DepositPage/views.tsx:191
#, c-format
msgid "Deposit fee"
-msgstr ""
+msgstr "Комісія за депозит"
#: src/wallet/DepositPage/views.tsx:205
#, c-format
msgid "Total deposit"
-msgstr ""
+msgstr "Загальний депозит"
#: src/wallet/DepositPage/views.tsx:233
#, c-format
msgid "Deposit&nbsp;%1$s %2$s"
-msgstr ""
+msgstr "Депозит&nbsp;%1$s %2$s"
#: src/wallet/AddAccount/views.tsx:56
#, c-format
msgid "Add bank account for %1$s"
-msgstr ""
+msgstr "Додати банківський рахунок для %1$s"
#: src/wallet/AddAccount/views.tsx:59
#, c-format
msgid "Enter the URL of an exchange you trust."
-msgstr ""
+msgstr "Введіть URL обмінника, якому ви довіряєте."
#: src/wallet/AddAccount/views.tsx:66
#, c-format
msgid "Unable add this account"
-msgstr ""
+msgstr "Не вдалося додати цей рахунок"
#: src/wallet/AddAccount/views.tsx:73
#, c-format
msgid "Select account type"
-msgstr ""
+msgstr "Виберіть тип рахунку"
#: src/wallet/ExchangeAddConfirm.tsx:42
#, c-format
msgid "Review terms of service"
-msgstr ""
+msgstr "Перегляньте умови обслуговування"
#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
msgid "Exchange URL"
-msgstr ""
+msgstr "URL обмінника"
#: src/wallet/ExchangeAddConfirm.tsx:70
#, c-format
msgid "Add exchange"
-msgstr ""
+msgstr "Додати обмінник"
#: src/wallet/ExchangeSetUrl.tsx:112
#, c-format
msgid "Add new exchange"
-msgstr ""
+msgstr "Додати новий обмінник"
#: src/wallet/ExchangeSetUrl.tsx:116
#, c-format
msgid "Add exchange for %1$s"
-msgstr ""
+msgstr "Додати обмінник для %1$s"
#: src/wallet/ExchangeSetUrl.tsx:128
#, c-format
msgid "An exchange has been found! Review the information and click next"
-msgstr ""
+msgstr "Знайдено обмінник! Перегляньте інформацію та натисніть \"Далі\""
#: src/wallet/ExchangeSetUrl.tsx:135
#, c-format
msgid "This exchange doesn&apos;t match the expected currency %1$s"
-msgstr ""
+msgstr "Цей обмінник не відповідає очікуваній валюті %1$s"
#: src/wallet/ExchangeSetUrl.tsx:143
#, c-format
msgid "Unable to verify this exchange"
-msgstr ""
+msgstr "Не вдалося перевірити цей обмінник"
#: src/wallet/ExchangeSetUrl.tsx:151
#, c-format
msgid "Unable to add this exchange"
-msgstr ""
+msgstr "Не вдалося додати цей обмінник"
#: src/wallet/ExchangeSetUrl.tsx:167
#, c-format
msgid "loading"
-msgstr ""
+msgstr "завантаження"
#: src/wallet/ExchangeSetUrl.tsx:174
#, c-format
msgid "Version"
-msgstr ""
+msgstr "Версія"
#: src/wallet/ExchangeSetUrl.tsx:206
#, c-format
msgid "Next"
-msgstr ""
+msgstr "Далі"
#: src/components/TransactionItem.tsx:201
#, c-format
msgid "Waiting for confirmation"
-msgstr ""
+msgstr "Очікування підтвердження"
#: src/components/TransactionItem.tsx:266
#, c-format
msgid "PENDING"
-msgstr ""
+msgstr "В ОБРОБЦІ"
#: src/wallet/History.tsx:75
#, c-format
msgid "Could not load the list of transactions"
-msgstr ""
+msgstr "Не вдалося завантажити список транзакцій"
#: src/wallet/History.tsx:233
#, c-format
msgid "Your transaction history is empty for this currency."
-msgstr ""
+msgstr "Ваша історія транзакцій порожня для цієї валюти."
#: src/wallet/ProviderAddPage.tsx:127
#, c-format
msgid "Add backup provider"
-msgstr ""
+msgstr "Додати постачальника резервного копіювання"
#: src/wallet/ProviderAddPage.tsx:131
#, c-format
msgid "Could not get provider information"
-msgstr ""
+msgstr "Не вдалося отримати інформацію про постачальника"
#: src/wallet/ProviderAddPage.tsx:140
#, c-format
msgid "Backup providers may charge for their service"
msgstr ""
+"Постачальники резервного копіювання можуть стягувати плату за свої послуги"
#: src/wallet/ProviderAddPage.tsx:147
#, c-format
msgid "URL"
-msgstr ""
+msgstr "URL"
#: src/wallet/ProviderAddPage.tsx:158
#, c-format
msgid "Name"
-msgstr ""
+msgstr "Назва"
#: src/wallet/ProviderAddPage.tsx:212
#, c-format
msgid "Provider URL"
-msgstr ""
+msgstr "URL постачальника"
#: src/wallet/ProviderAddPage.tsx:218
#, c-format
msgid "Please review and accept this provider&apos;s terms of service"
msgstr ""
+"Будь ласка, перегляньте та прийміть Умови надання послуг цього постачальника"
#: src/wallet/ProviderAddPage.tsx:223
#, c-format
msgid "Pricing"
-msgstr ""
+msgstr "Ціни"
#: src/wallet/ProviderAddPage.tsx:226
#, c-format
msgid "free of charge"
-msgstr ""
+msgstr "безкоштовно"
#: src/wallet/ProviderAddPage.tsx:228
#, c-format
msgid "%1$s per year of service"
-msgstr ""
+msgstr "%1$s на рік обслуговування"
#: src/wallet/ProviderAddPage.tsx:235
#, c-format
msgid "Storage"
-msgstr ""
+msgstr "Сховище"
#: src/wallet/ProviderAddPage.tsx:238
#, c-format
msgid "%1$s megabytes of storage per year of service"
-msgstr ""
+msgstr "%1$s мегабайт сховища на рік обслуговування"
#: src/wallet/ProviderAddPage.tsx:244
#, c-format
msgid "Accept terms of service"
-msgstr ""
+msgstr "Прийняти умови надання послуг"
#: src/wallet/ReserveCreated.tsx:44
#, c-format
msgid "Could not parse the payto URI"
-msgstr ""
+msgstr "Не вдалося розібрати URI для оплати"
#: src/wallet/ReserveCreated.tsx:45
#, c-format
msgid "Please check the uri"
-msgstr ""
+msgstr "Будь ласка, перевірте URI"
#: src/wallet/ReserveCreated.tsx:75
#, c-format
msgid "Exchange is ready for withdrawal"
-msgstr ""
+msgstr "Обмінник готовий до зняття коштів"
#: src/wallet/ReserveCreated.tsx:78
#, c-format
msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
msgstr ""
+"Щоб завершити процес, вам потрібно переказати %1$s %2$s на банківський "
+"рахунок обмінника"
#: src/wallet/ReserveCreated.tsx:87
#, c-format
@@ -1507,31 +1538,33 @@ msgid ""
"Alternative, you can also scan this QR code or open %1$s if you have a banking "
"app installed that supports RFC 8905"
msgstr ""
+"Або ви можете відсканувати цей QR-код або відкрити %1$s, якщо у вас "
+"встановлений банківський додаток, який підтримує RFC 8905"
#: src/wallet/ReserveCreated.tsx:98
#, c-format
msgid "Cancel withdrawal"
-msgstr ""
+msgstr "Скасувати зняття"
#: src/wallet/Settings.tsx:115
#, c-format
msgid "Could not toggle auto-open"
-msgstr ""
+msgstr "Не вдалося перемкнути автоматичне відкриття"
#: src/wallet/Settings.tsx:121
#, c-format
msgid "Could not toggle clipboard"
-msgstr ""
+msgstr "Не вдалося перемкнути буфер обміну"
#: src/wallet/Settings.tsx:126
#, c-format
msgid "Navigator"
-msgstr ""
+msgstr "Навігатор"
#: src/wallet/Settings.tsx:129
#, c-format
msgid "Automatically open wallet based on page content"
-msgstr ""
+msgstr "Автоматично відкривати гаманець на основі вмісту сторінки"
#: src/wallet/Settings.tsx:135
#, c-format
@@ -1539,111 +1572,113 @@ msgid ""
"Enabling this option below will make using the wallet faster, but requires more "
"permissions from your browser."
msgstr ""
+"Увімкнення цієї опції зробить використання гаманця швидшим, але вимагає "
+"більше дозволів від вашого браузера."
#: src/wallet/Settings.tsx:145
#, c-format
msgid "Automatically check clipboard for Taler URI"
-msgstr ""
+msgstr "Автоматично перевіряти буфер обміну на наявність Taler URI"
#: src/wallet/Settings.tsx:162
#, c-format
msgid "Trust"
-msgstr ""
+msgstr "Довіряти"
#: src/wallet/Settings.tsx:166
#, c-format
msgid "No exchange yet"
-msgstr ""
+msgstr "Ще немає обмінника"
#: src/wallet/Settings.tsx:180
#, c-format
msgid "Term of Service"
-msgstr ""
+msgstr "Умови обслуговування"
#: src/wallet/Settings.tsx:191
#, c-format
msgid "ok"
-msgstr ""
+msgstr "ок"
#: src/wallet/Settings.tsx:197
#, c-format
msgid "changed"
-msgstr ""
+msgstr "змінено"
#: src/wallet/Settings.tsx:204
#, c-format
msgid "not accepted"
-msgstr ""
+msgstr "не прийнято"
#: src/wallet/Settings.tsx:210
#, c-format
msgid "unknown (exchange status should be updated)"
-msgstr ""
+msgstr "невідомо (статус обмінника має бути оновлено)"
#: src/wallet/Settings.tsx:236
#, c-format
msgid "Add an exchange"
-msgstr ""
+msgstr "Додати обмінник"
#: src/wallet/Settings.tsx:241
#, c-format
msgid "Troubleshooting"
-msgstr ""
+msgstr "Вирішення проблем"
#: src/wallet/Settings.tsx:244
#, c-format
msgid "Developer mode"
-msgstr ""
+msgstr "Режим розробника"
#: src/wallet/Settings.tsx:246
#, c-format
msgid "More options and information useful for debugging"
-msgstr ""
+msgstr "Більше опцій та інформації, корисної для відлагодження"
#: src/wallet/Settings.tsx:257
#, c-format
msgid "Display"
-msgstr ""
+msgstr "Дисплей"
#: src/wallet/Settings.tsx:261
#, c-format
msgid "Current Language"
-msgstr ""
+msgstr "Поточна мова"
#: src/wallet/Settings.tsx:274
#, c-format
msgid "Wallet Core"
-msgstr ""
+msgstr "Ядро гаманця"
#: src/wallet/Settings.tsx:284
#, c-format
msgid "Web Extension"
-msgstr ""
+msgstr "Веб-розширення"
#: src/wallet/Settings.tsx:295
#, c-format
msgid "Exchange compatibility"
-msgstr ""
+msgstr "Сумісність з обмінником"
#: src/wallet/Settings.tsx:299
#, c-format
msgid "Merchant compatibility"
-msgstr ""
+msgstr "Сумісність з продавцем"
#: src/wallet/Settings.tsx:303
#, c-format
msgid "Bank compatibility"
-msgstr ""
+msgstr "Сумісність з банком"
#: src/wallet/Welcome.tsx:59
#, c-format
msgid "Browser Extension Installed!"
-msgstr ""
+msgstr "Розширення для браузера встановлено!"
#: src/wallet/Welcome.tsx:63
#, c-format
msgid "You can open the GNU Taler Wallet using the combination %1$s ."
-msgstr ""
+msgstr "Ви можете відкрити гаманець GNU Taler за допомогою комбінації %1$s."
#: src/wallet/Welcome.tsx:72
#, c-format
@@ -1651,26 +1686,28 @@ msgid ""
"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
"access without keyboard:"
msgstr ""
+"Також закріплення гаманця GNU Taler у вашому браузері Chrome дозволяє швидко "
+"отримати доступ без клавіатури:"
#: src/wallet/Welcome.tsx:79
#, c-format
msgid "Click the puzzle icon"
-msgstr ""
+msgstr "Натисніть на іконку пазлу"
#: src/wallet/Welcome.tsx:82
#, c-format
msgid "Search for GNU Taler Wallet"
-msgstr ""
+msgstr "Знайдіть гаманець GNU Taler"
#: src/wallet/Welcome.tsx:85
#, c-format
msgid "Click the pin icon"
-msgstr ""
+msgstr "Натисніть на іконку шпильки"
#: src/wallet/Welcome.tsx:91
#, c-format
msgid "Permissions"
-msgstr ""
+msgstr "Дозволи"
#: src/wallet/Welcome.tsx:100
#, c-format
@@ -1678,31 +1715,33 @@ msgid ""
"(Enabling this option below will make using the wallet faster, but requires more "
"permissions from your browser.)"
msgstr ""
+"(Увімкнення цієї опції нижче зробить використання гаманця швидшим, але "
+"вимагає більше дозволів від вашого браузера.)"
#: src/wallet/Welcome.tsx:110
#, c-format
msgid "Next Steps"
-msgstr ""
+msgstr "Наступні кроки"
#: src/wallet/Welcome.tsx:113
#, c-format
msgid "Try the demo"
-msgstr ""
+msgstr "Спробуйте демо"
#: src/wallet/Welcome.tsx:116
#, c-format
msgid "Learn how to top up your wallet balance"
-msgstr ""
+msgstr "Дізнайтеся, як поповнити баланс вашого гаманця"
#: src/components/Diagnostics.tsx:31
#, c-format
msgid "Diagnostics timed out. Could not talk to the wallet backend."
-msgstr ""
+msgstr "Час діагностики вичерпано. Не вдалося зв'язатися з бекендом гаманця."
#: src/components/Diagnostics.tsx:52
#, c-format
msgid "Problems detected:"
-msgstr ""
+msgstr "Виявлено проблеми:"
#: src/components/Diagnostics.tsx:61
#, c-format
@@ -1710,6 +1749,8 @@ msgid ""
"Please check in your %1$s settings that you have IndexedDB enabled (check the "
"preference name %2$s)."
msgstr ""
+"Будь ласка, перевірте у налаштуваннях %1$s, що у вас увімкнено IndexedDB ("
+"перевірте назву налаштування %2$s)."
#: src/components/Diagnostics.tsx:70
#, c-format
@@ -1717,16 +1758,18 @@ msgid ""
"Your wallet database is outdated. Currently automatic migration is not "
"supported. Please go %1$s to reset the wallet database."
msgstr ""
+"База даних вашого гаманця застаріла. Наразі автоматична міграція не "
+"підтримується. Будь ласка, перейдіть до %1$s, щоб скинути базу даних гаманця."
#: src/components/Diagnostics.tsx:83
#, c-format
msgid "Running diagnostics"
-msgstr ""
+msgstr "Запуск діагностики"
#: src/wallet/DeveloperPage.tsx:163
#, c-format
msgid "Debug tools"
-msgstr ""
+msgstr "Інструменти відлагодження"
#: src/wallet/DeveloperPage.tsx:170
#, c-format
@@ -1734,223 +1777,225 @@ msgid ""
"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
"YOUR COINS?"
msgstr ""
+"Ви хочете НЕВІДВОРОТНО ЗНИЩИТИ все всередині вашого гаманця і ВТРАТИТИ ВСІ "
+"СВОЇ МОНЕТИ?"
#: src/wallet/DeveloperPage.tsx:176
#, c-format
msgid "reset"
-msgstr ""
+msgstr "скинути"
#: src/wallet/DeveloperPage.tsx:183
#, c-format
msgid "TESTING: This may delete all your coin, proceed with caution"
-msgstr ""
+msgstr "ТЕСТУВАННЯ: Це може видалити всі ваші монети, будьте обережні"
#: src/wallet/DeveloperPage.tsx:189
#, c-format
msgid "run gc"
-msgstr ""
+msgstr "запустити gc"
#: src/wallet/DeveloperPage.tsx:197
#, c-format
msgid "import database"
-msgstr ""
+msgstr "імпортувати базу даних"
#: src/wallet/DeveloperPage.tsx:219
#, c-format
msgid "export database"
-msgstr ""
+msgstr "експортувати базу даних"
#: src/wallet/DeveloperPage.tsx:225
#, c-format
msgid "Database exported at %1$s %2$s to download"
-msgstr ""
+msgstr "База даних експортована о %1$s %2$s для завантаження"
#: src/wallet/DeveloperPage.tsx:248
#, c-format
msgid "Coins"
-msgstr ""
+msgstr "Монети"
#: src/wallet/DeveloperPage.tsx:282
#, c-format
msgid "Pending operations"
-msgstr ""
+msgstr "Очікувані операції"
#: src/wallet/DeveloperPage.tsx:328
#, c-format
msgid "usable coins"
-msgstr ""
+msgstr "використовувані монети"
#: src/wallet/DeveloperPage.tsx:337
#, c-format
msgid "id"
-msgstr ""
+msgstr "ідентифікатор"
#: src/wallet/DeveloperPage.tsx:340
#, c-format
msgid "denom"
-msgstr ""
+msgstr "номінал"
#: src/wallet/DeveloperPage.tsx:343
#, c-format
msgid "value"
-msgstr ""
+msgstr "значення"
#: src/wallet/DeveloperPage.tsx:346
#, c-format
msgid "status"
-msgstr ""
+msgstr "статус"
#: src/wallet/DeveloperPage.tsx:349
#, c-format
msgid "from refresh?"
-msgstr ""
+msgstr "з оновлення?"
#: src/wallet/DeveloperPage.tsx:352
#, c-format
msgid "age key count"
-msgstr ""
+msgstr "кількість ключів за віком"
#: src/wallet/DeveloperPage.tsx:369
#, c-format
msgid "spent coins"
-msgstr ""
+msgstr "витрачені монети"
#: src/wallet/DeveloperPage.tsx:373
#, c-format
msgid "click to show"
-msgstr ""
+msgstr "натисніть, щоб показати"
#: src/wallet/QrReader.tsx:108
#, c-format
msgid "Scan a QR code or enter taler:// URI below"
-msgstr ""
+msgstr "Скануйте QR-код або введіть taler:// URI нижче"
#: src/wallet/QrReader.tsx:122
#, c-format
msgid "Open"
-msgstr "Доступні"
+msgstr "Відкрити"
#: src/wallet/QrReader.tsx:128
#, c-format
msgid "URI is not valid. Taler URI should start with `taler://`"
-msgstr ""
+msgstr "URI недійсний. Taler URI повинен починатися з `taler://`"
#: src/wallet/QrReader.tsx:133
#, c-format
msgid "Try another"
-msgstr ""
+msgstr "Спробуйте інший"
#: src/wallet/DestinationSelection.tsx:183
#, c-format
msgid "Could not load list of exchange"
-msgstr ""
+msgstr "Не вдалося завантажити список обмінників"
#: src/wallet/DestinationSelection.tsx:209
#, c-format
msgid "Choose a currency to proceed or add another exchange"
-msgstr ""
+msgstr "Виберіть валюту для продовження або додайте інший обмінник"
#: src/wallet/DestinationSelection.tsx:217
#, c-format
msgid "Known currencies"
-msgstr ""
+msgstr "Відомі валюти"
#: src/wallet/DestinationSelection.tsx:318
#, c-format
msgid "Specify the amount and the origin"
-msgstr ""
+msgstr "Вкажіть суму та джерело"
#: src/wallet/DestinationSelection.tsx:336
#, c-format
msgid "Change currency"
-msgstr ""
+msgstr "Змінити валюту"
#: src/wallet/DestinationSelection.tsx:344
#, c-format
msgid "Use previous origins:"
-msgstr ""
+msgstr "Використовувати попередні джерела:"
#: src/wallet/DestinationSelection.tsx:364
#, c-format
msgid "Or specify the origin of the money"
-msgstr ""
+msgstr "Або вкажіть джерело грошей"
#: src/wallet/DestinationSelection.tsx:372
#, c-format
msgid "Specify the origin of the money"
-msgstr ""
+msgstr "Вкажіть джерело грошей"
#: src/wallet/DestinationSelection.tsx:380
#, c-format
msgid "From my bank account"
-msgstr ""
+msgstr "З мого банківського рахунку"
#: src/wallet/DestinationSelection.tsx:395
#, c-format
msgid "From another wallet"
-msgstr ""
+msgstr "З іншого гаманця"
#: src/wallet/DestinationSelection.tsx:449
#, c-format
msgid "currency not provided"
-msgstr ""
+msgstr "валюта не вказана"
#: src/wallet/DestinationSelection.tsx:459
#, c-format
msgid "Specify the amount and the destination"
-msgstr ""
+msgstr "Вкажіть суму та місце призначення"
#: src/wallet/DestinationSelection.tsx:483
#, c-format
msgid "Use previous destinations:"
-msgstr ""
+msgstr "Використовувати попередні місця призначення:"
#: src/wallet/DestinationSelection.tsx:503
#, c-format
msgid "Or specify the destination of the money"
-msgstr ""
+msgstr "Або вкажіть місце призначення грошей"
#: src/wallet/DestinationSelection.tsx:511
#, c-format
msgid "Specify the destination of the money"
-msgstr ""
+msgstr "Вкажіть адрес призначення грошей"
#: src/wallet/DestinationSelection.tsx:521
#, c-format
msgid "To my bank account"
-msgstr ""
+msgstr "На мій банківський рахунок"
#: src/wallet/DestinationSelection.tsx:534
#, c-format
msgid "To another wallet"
-msgstr ""
+msgstr "На інший гаманець"
#: src/cta/Recovery/views.tsx:30
#, c-format
msgid "Could not load backup recovery information"
-msgstr ""
+msgstr "Не вдалося завантажити інформацію для відновлення резервної копії"
#: src/cta/Recovery/views.tsx:47
#, c-format
msgid "Digital wallet recovery"
-msgstr ""
+msgstr "Відновлення цифрового гаманця"
#: src/cta/Recovery/views.tsx:52
#, c-format
msgid "Import backup, show info"
-msgstr ""
+msgstr "Імпортувати резервну копію, показати інформацію"
#: src/wallet/Application.tsx:189
#, c-format
msgid "All done, your transaction is in progress"
-msgstr ""
+msgstr "Все готово, ваша транзакція в процесі"
#: src/components/EditableText.tsx:45
#, c-format
msgid "Edit"
-msgstr ""
+msgstr "Редагувати"
#: src/wallet/ManualWithdrawPage.tsx:102
#, c-format
msgid "Could not load the list of known exchanges"
-msgstr ""
+msgstr "Не вдалося завантажити список відомих обмінників"
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index 3c116fab2..2388647c1 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -227,6 +227,7 @@ export interface BackgroundPlatformAPI {
listenToAllChannels(
notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
message: MessageFromFrontend<Op> & { id: string },
+ from: string,
) => Promise<MessageResponse>,
): void;
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index 056351e3f..3f6708fc0 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -35,6 +35,7 @@ import {
Settings,
defaultSettings,
} from "./api.js";
+import { encodeCrockForURI } from "@gnu-taler/web-util/browser";
const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
isFirefox,
@@ -53,7 +54,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels ,
+ listenToAllChannels,
registerReloadOnNewVersion,
sendMessageToAllChannels,
openNewURLFromPopup,
@@ -178,54 +179,54 @@ function openWalletURIFromPopup(uri: TalerUri): void {
case TalerUriAction.WithdrawExchange:
case TalerUriAction.Withdraw:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/withdraw?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/withdraw?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.Restore:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/recovery?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/recovery?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.Pay:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
+ `static/wallet.html#/cta/pay?talerUri=${encodeCrockForURI(talerUri)}`,
);
break;
case TalerUriAction.Refund:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/refund?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/refund?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.PayPull:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/invoice/pay?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/invoice/pay?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.PayPush:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.PayTemplate:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/pay/template?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/pay/template?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
break;
case TalerUriAction.AddExchange:
url = chrome.runtime.getURL(
- `static/wallet.html#/cta/add/exchange?talerUri=${encodeURIComponent(
+ `static/wallet.html#/cta/add/exchange?talerUri=${encodeCrockForURI(
talerUri,
)}`,
);
@@ -276,18 +277,20 @@ async function sendMessageToBackground<
Op extends WalletOperations | BackgroundOperations,
>(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
- const messageWithId = { ...message, id: `id_${nextMessageIndex}` };
+ const messageWithId = { ...message, id: `fg:${nextMessageIndex}` };
return new Promise<MessageResponse>((resolve, reject) => {
logger.trace("send operation to the wallet background", message);
let timedout = false;
const timerId = setTimeout(() => {
timedout = true;
- reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
- requestMethod: "wallet",
- requestUrl: message.operation,
- timeoutMs: 20 * 1000,
- }));
+ reject(
+ TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }),
+ );
}, 20 * 1000);
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
if (timedout) {
@@ -309,7 +312,9 @@ async function sendMessageToBackground<
* To be used by the foreground
*/
let notificationPort: chrome.runtime.Port | undefined;
-function listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void {
+function listenToWalletBackground(
+ listener: (message: MessageFromBackend) => void,
+): () => void {
if (notificationPort === undefined) {
notificationPort = chrome.runtime.connect({ name: "notifications" });
}
@@ -380,13 +385,18 @@ function registerAllIncomingConnections(): void {
});
}
+function createTabId(tab: chrome.tabs.Tab | undefined) {
+ return !tab ? "popup" : `${tab.windowId}:${tab.id}`;
+}
+
function listenToAllChannels(
notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
message: MessageFromFrontend<Op> & { id: string },
+ id: string,
) => Promise<MessageResponse>,
): void {
chrome.runtime.onMessage.addListener((message, sender, reply) => {
- notifyNewMessage(message)
+ notifyNewMessage(message, createTabId(sender.tab))
.then((apiResponse) => {
try {
reply(apiResponse);
@@ -483,26 +493,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D
extends CanvasState,
- CanvasTransform,
- CanvasCompositing,
- CanvasImageSmoothing,
- CanvasFillStrokeStyles,
- CanvasShadowStyles,
- CanvasFilters,
- CanvasRect,
- CanvasDrawPath,
- CanvasUserInterface,
- CanvasText,
- CanvasDrawImage,
- CanvasImageData,
- CanvasPathDrawingStyles,
- CanvasTextDrawingStyles,
- CanvasPath {
+ CanvasTransform,
+ CanvasCompositing,
+ CanvasImageSmoothing,
+ CanvasFillStrokeStyles,
+ CanvasShadowStyles,
+ CanvasFilters,
+ CanvasRect,
+ CanvasDrawPath,
+ CanvasUserInterface,
+ CanvasText,
+ CanvasDrawImage,
+ CanvasImageData,
+ CanvasPathDrawingStyles,
+ CanvasTextDrawingStyles,
+ CanvasPath {
readonly canvas: OffscreenCanvas;
}
declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D;
- new(): OffscreenCanvasRenderingContext2D;
+ new (): OffscreenCanvasRenderingContext2D;
};
interface OffscreenCanvas extends EventTarget {
@@ -515,7 +525,7 @@ interface OffscreenCanvas extends EventTarget {
}
declare const OffscreenCanvas: {
prototype: OffscreenCanvas;
- new(width: number, height: number): OffscreenCanvas;
+ new (width: number, height: number): OffscreenCanvas;
};
function createCanvas(size: number): OffscreenCanvas {
@@ -760,7 +770,6 @@ function listenNetworkConnectionState(
};
}
-
function runningOnPrivateMode(): boolean {
return chrome.extension.inIncognitoContext;
}
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index b53e8f3c4..844a5c517 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -95,7 +95,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
useServiceWorkerAsBackgroundProcess: () => false,
listenToAllChannels: (
- notifyNewMessage: (message: any) => Promise<MessageResponse>,
+ notifyNewMessage: (message: any, from: string) => Promise<MessageResponse>,
) => {
window.addEventListener(
"message",
@@ -103,7 +103,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
if (event.data.type !== "command") return;
const sender = event.data.header.replyMe;
- notifyNewMessage(event.data.body as any).then((resp) => {
+ notifyNewMessage(event.data.body as any, sender).then((resp) => {
logger.trace(`listenToAllChannels: from ${sender}`, event);
if (event.source) {
const msg: IframeMessageResponse = {
@@ -199,4 +199,3 @@ interface IframeMessageCommand {
}
export default api;
-
diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts
index 3d67423fd..4292bd7fd 100644
--- a/packages/taler-wallet-webextension/src/platform/firefox.ts
+++ b/packages/taler-wallet-webextension/src/platform/firefox.ts
@@ -18,9 +18,8 @@ import {
BackgroundPlatformAPI,
CrossBrowserPermissionsApi,
ForegroundPlatformAPI,
- Permissions,
Settings,
- defaultSettings,
+ defaultSettings
} from "./api.js";
import chromePlatform, {
containsClipboardPermissions as chromeClipContains,
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx
index cbb9b50b2..e971c4375 100644
--- a/packages/taler-wallet-webextension/src/popup/Application.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -20,21 +20,24 @@
* @author sebasjm
*/
+import { ScopeInfo, stringifyScopeInfoShort } from "@gnu-taler/taler-util";
import {
TranslationProvider,
useTranslationContext,
+ encodeCrockForURI,
+ decodeCrockFromURI,
} from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { route, Route, Router } from "preact-router";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
import { useEffect, useState } from "preact/hooks";
+import { Pages, PopupNavBar, PopupNavBarOptions } from "../NavigationBar.js";
import PendingTransactions from "../components/PendingTransactions.js";
import { PopupBox } from "../components/styled/index.js";
import { AlertProvider } from "../context/alert.js";
import { IoCProviderForRuntime } from "../context/iocContext.js";
import { useTalerActionURL } from "../hooks/useTalerActionURL.js";
import { strings } from "../i18n/strings.js";
-import { Pages, PopupNavBar, PopupNavBarOptions } from "../NavigationBar.js";
import { platform } from "../platform/foreground.js";
import { BackupPage } from "../wallet/BackupPage.js";
import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
@@ -59,7 +62,7 @@ function ApplicationView(): VNode {
useEffect(() => {
if (actionUri) {
- route(Pages.cta({ action: encodeURIComponent(actionUri) }));
+ route(Pages.cta({ action: encodeCrockForURI(actionUri) }));
}
}, [actionUri]);
@@ -68,7 +71,7 @@ function ApplicationView(): VNode {
}
function redirectToURL(str: string): void {
- platform.openNewURLFromPopup(new URL(str))
+ platform.openNewURLFromPopup(new URL(str));
}
return (
@@ -76,14 +79,34 @@ function ApplicationView(): VNode {
<Route
path={Pages.balance}
component={() => (
- <PopupTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <PopupTemplate
+ path="balance"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<BalancePage
- goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))}
- goToWalletDeposit={(currency: string) =>
- redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ goToWalletManualWithdraw={(scope?: ScopeInfo) => {
+ return redirectTo(
+ Pages.receiveCash({
+ scope: !scope
+ ? undefined
+ : encodeCrockForURI(stringifyScopeInfoShort(scope)),
+ }),
+ );
+ }}
+ goToWalletDeposit={(scope: ScopeInfo) =>
+ redirectTo(
+ Pages.sendCash({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(scope)),
+ }),
+ )
}
- goToWalletHistory={(currency: string) =>
- redirectTo(Pages.balanceHistory({ currency }))
+ goToWalletHistory={(scope: ScopeInfo) =>
+ redirectTo(
+ Pages.balanceHistory({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(scope)),
+ }),
+ )
}
/>
</PopupTemplate>
@@ -98,7 +121,7 @@ function ApplicationView(): VNode {
return (
<PopupTemplate goToURL={redirectToURL}>
<TalerActionFound
- url={decodeURIComponent(action)}
+ url={decodeCrockFromURI(action)}
onDismiss={() => {
setDismissed(true);
return redirectTo(Pages.balance);
@@ -112,7 +135,11 @@ function ApplicationView(): VNode {
<Route
path={Pages.backup}
component={() => (
- <PopupTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <PopupTemplate
+ path="backup"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<BackupPage
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
@@ -127,9 +154,9 @@ function ApplicationView(): VNode {
onPayProvider={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
- onWithdraw={(amount: string) =>
- redirectTo(Pages.receiveCash({ amount }))
- }
+ onWithdraw={async (_amount: string) => {
+ // redirectTo(Pages.receiveCash({ amount }))
+ }}
pid={pid}
onBack={() => redirectTo(Pages.backup)}
/>
@@ -219,7 +246,10 @@ function PopupTemplate({
}): VNode {
return (
<Fragment>
- <PendingTransactions goToTransaction={goToTransaction} goToURL={goToURL} />
+ <PendingTransactions
+ goToTransaction={goToTransaction}
+ goToURL={goToURL}
+ />
<PopupNavBar path={path} />
<PopupBox>
<AlertProvider>{children}</AlertProvider>
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 73bd8e96d..e730448ac 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -17,6 +17,7 @@
import {
Amounts,
NotificationType,
+ ScopeInfo,
WalletBalance,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -41,9 +42,9 @@ import { AddNewActionView } from "../wallet/AddNewActionView.js";
import { NoBalanceHelp } from "./NoBalanceHelp.js";
export interface Props {
- goToWalletDeposit: (currency: string) => Promise<void>;
- goToWalletHistory: (currency: string) => Promise<void>;
- goToWalletManualWithdraw: () => Promise<void>;
+ goToWalletDeposit: (scope: ScopeInfo) => Promise<void>;
+ goToWalletHistory: (scope: ScopeInfo) => Promise<void>;
+ goToWalletManualWithdraw: (scope?: ScopeInfo) => Promise<void>;
}
export type State = State.Loading | State.Error | State.Action | State.Balances;
@@ -70,8 +71,8 @@ export namespace State {
error: undefined;
balances: WalletBalance[];
addAction: ButtonHandler;
- goToWalletDeposit: (currency: string) => Promise<void>;
- goToWalletHistory: (currency: string) => Promise<void>;
+ goToWalletDeposit: (currency: ScopeInfo) => Promise<void>;
+ goToWalletHistory: (currency: ScopeInfo) => Promise<void>;
goToWalletManualWithdraw: ButtonHandler;
}
}
@@ -105,8 +106,7 @@ function useComponentState({
if (state.hasError) {
return {
status: "error",
- error: alertFromError( i18n,
- i18n.str`Could not load the balance`, state),
+ error: alertFromError(i18n, i18n.str`Could not load the balance`, state),
};
}
if (addingAction) {
@@ -126,7 +126,9 @@ function useComponentState({
onClick: pushAlertOnError(async () => setAddingAction(true)),
},
goToWalletManualWithdraw: {
- onClick: pushAlertOnError(goToWalletManualWithdraw),
+ onClick: pushAlertOnError(async () => {
+ goToWalletManualWithdraw(state.response.balances.length ? state.response.balances[0].scopeInfo : undefined);
+ }),
},
goToWalletDeposit,
goToWalletHistory,
@@ -155,8 +157,8 @@ export function BalanceView(state: State.Balances): VNode {
const currencyWithNonZeroAmount = state.balances
.filter((b) => !Amounts.isZero(b.available))
.map((b) => {
- b.flags
- return b.available.split(":")[0]
+ b.flags;
+ return b.scopeInfo;
});
if (state.balances.length === 0) {
@@ -172,7 +174,10 @@ export function BalanceView(state: State.Balances): VNode {
<section>
<BalanceTable
balances={state.balances}
- goToWalletHistory={state.goToWalletHistory}
+ goToWalletHistory={(e) => {
+ console.log("qwe", e);
+ state.goToWalletHistory(e);
+ }}
/>
</section>
<footer style={{ justifyContent: "space-between" }}>
@@ -184,7 +189,7 @@ export function BalanceView(state: State.Balances): VNode {
</Button>
{currencyWithNonZeroAmount.length > 0 && (
<MultiActionButton
- label={(s) => i18n.str`Send ${s}`}
+ label={(s) => i18n.str`Send ${s.currency}`}
actions={currencyWithNonZeroAmount}
onClick={(c) => state.goToWalletDeposit(c)}
/>
diff --git a/packages/taler-wallet-webextension/src/pwa/index.html b/packages/taler-wallet-webextension/src/pwa/index.html
index c150ee68d..da1bcc479 100644
--- a/packages/taler-wallet-webextension/src/pwa/index.html
+++ b/packages/taler-wallet-webextension/src/pwa/index.html
@@ -3,18 +3,56 @@
<meta charset="utf-8" />
<link rel="manifest" href="./manifest.json" />
<style>
+ /* Normalize font-family, rather than letting the UA decide */
+ html {
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ "Open Sans",
+ "Helvetica Neue",
+ sans-serif;
+ }
+
+ /* Setup the popup overlay */
.overlay {
- position: absolute;
+ /* TODO: Consider moving it to the top right of the screen, like an actual popup usually is */
+ position: fixed;
top: 0px;
+ left: 0px;
display: none;
width: 100%;
height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: #0007;
+ backdrop-filter: blur(12px);
color: white;
justify-content: center;
}
.overlay > iframe {
margin: auto;
+ border: 1px solid #666;
+ }
+ #wallet-window {
+ border: 1px solid #666;
+ border-radius: 4px;
+ /* TODO: why arbitrary 38px? also why no flexbox? */
+ height: calc(100% - 38px);
+ width: min(850px, calc(100% - 8px));
+ }
+
+ /* firefox's native button styles more or less, because conistency is good */
+ button {
+ background: #e9e9ed;
+ color: #151516;
+ border: 1px solid #828282;
+ border-radius: 4px;
+ }
+ button:hover {
+ background: #c0c0c0;
}
</style>
</head>
@@ -37,53 +75,26 @@
redirectWallet("about:blank");
}
function reloadWallet() {
- window.frames["wallet"].location.reload()
+ window.frames["wallet"].location.reload();
}
function openPage() {
window.frames["other"].location =
document.getElementById("page-url").value;
}
</script>
- <button value="asd" onclick="openPopup()">open popup</button>
- <button value="asd" onclick="closeWallet();openWallet()">
- restart
- </button>
- <button value="asd" onclick="reloadWallet()">
- refresh
- </button>
- <br />
- <iframe
- id="wallet-window"
- name="wallet"
- src="wallet.html"
- style="height: calc(100% - 30px)"
- width="850"
- height="90%"
- >
+ <button onclick="openPopup()">Open Popup</button>
+ <button onclick="closeWallet();openWallet()">Restart Wallet</button>
+ <button onclick="reloadWallet()">Refresh Frame</button>
+ <div style="height: 8px"></div>
+ <iframe id="wallet-window" name="wallet" src="wallet.html" width="">
</iframe>
- <!-- <input id="page-url" type="text" />
- <button onclick="openPage()">open</button> -->
- <!-- <a
- href='javascript:void(window.frames["other"].location = "http://bank.taler:5882")'
- >open local bank</a
- >
- <hr />
- <iframe
- id="other-window"
- name="other"
- src="http://bank.taler:5882"
- width="100%"
- height="325"
- >
- </iframe> -->
<div class="overlay" id="popup-overlay" onclick="closePopup()">
-
<iframe
- id="popup-window"
- name="popup"
- src="about:blank"
- width="500"
- height="325"
+ id="popup-window"
+ name="popup"
+ src="about:blank"
+ width="500"
+ height="325"
>
</iframe>
</div>
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
index 3b7cbcbb7..37523c667 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -14,8 +14,16 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CoreApiResponse, TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
+import {
+ CoreApiResponse,
+ TalerError,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+
import type { MessageFromBackend } from "./platform/api.js";
+// FIXME: mem leak problems
+// import { encodeCrockForURI } from "@gnu-taler/web-util/browser";
+
/**
* This will modify all the pages that the user load when navigating with Web Extension enabled
@@ -51,8 +59,6 @@ const rootElementIsHTML =
// "meta[name=taler-support]",
// );
-
-
function validateTalerUri(uri: string): boolean {
return (
!!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
@@ -61,7 +67,11 @@ function validateTalerUri(uri: string): boolean {
function convertURIToWebExtensionPath(uri: string) {
const url = new URL(
- chrome.runtime.getURL(`static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`),
+ chrome.runtime.getURL(
+ // FIXME: mem leak problems
+ // `static/wallet.html#/taler-uri/${encodeCrockForURI(uri)}`,
+ `static/wallet.html#/taler-uri-simple/${encodeURIComponent(uri)}`,
+ ),
);
return url.href;
}
@@ -87,7 +97,7 @@ const logger = {
/**
*/
function redirectToTalerActionHandler(element: HTMLMetaElement) {
- const name = element.getAttribute("name")
+ const name = element.getAttribute("name");
if (!name) return;
if (name !== "taler-uri") return;
const uri = element.getAttribute("content");
@@ -98,20 +108,20 @@ function redirectToTalerActionHandler(element: HTMLMetaElement) {
return;
}
- const walletPage = convertURIToWebExtensionPath(uri)
- window.location.replace(walletPage)
+ const walletPage = convertURIToWebExtensionPath(uri);
+ window.location.replace(walletPage);
}
function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
- const meta = head.querySelector("meta[name=taler-support]")
+ const meta = head.querySelector("meta[name=taler-support]");
if (!meta) return;
const content = meta.getAttribute("content");
if (!content) return;
- const features = content.split(",")
+ const features = content.split(",");
const debugEnabled = meta.getAttribute("debug") === "true";
- const hijackEnabled = features.indexOf("uri") !== -1
- const talerApiEnabled = features.indexOf("api") !== -1 && trusted
+ const hijackEnabled = features.indexOf("uri") !== -1;
+ const talerApiEnabled = features.indexOf("api") !== -1 && trusted;
const scriptTag = document.createElement("script");
scriptTag.setAttribute("async", "false");
@@ -131,14 +141,16 @@ function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
scriptTag.src = url.href;
try {
- head.insertBefore(scriptTag, head.children.length ? head.children[0] : null);
+ head.insertBefore(
+ scriptTag,
+ head.children.length ? head.children[0] : null,
+ );
} catch (e) {
logger.info("inserting link handler failed!");
logger.error(e);
}
}
-
export interface ExtensionOperations {
isAutoOpenEnabled: {
request: void;
@@ -177,31 +189,38 @@ async function callBackground<Op extends keyof ExtensionOperations>(
return response.result as any;
}
-
let nextMessageIndex = 0;
/**
- *
- * @param message
- * @returns
+ *
+ * @param message
+ * @returns
*/
async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
message: MessageFromExtension<Op>,
): Promise<MessageResponse> {
- const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
+ const messageWithId = { ...message, id: `ld:${nextMessageIndex++ % 1000}` };
if (!chrome.runtime.id) {
- return Promise.reject(TalerError.fromDetail(TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, {}))
+ return Promise.reject(
+ TalerError.fromDetail(TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, {}),
+ );
}
return new Promise<any>((resolve, reject) => {
- logger.debug("send operation to the wallet background", message, chrome.runtime.id);
+ logger.debug(
+ "send operation to the wallet background",
+ message,
+ chrome.runtime.id,
+ );
let timedout = false;
const timerId = setTimeout(() => {
timedout = true;
- reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
- requestMethod: "wallet",
- requestUrl: message.operation,
- timeoutMs: 20 * 1000,
- }))
+ reject(
+ TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }),
+ );
}, 20 * 1000); //five seconds
try {
chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
@@ -218,7 +237,7 @@ async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
return true;
});
} catch (e) {
- console.log(e)
+ console.log(e);
}
});
}
@@ -240,71 +259,76 @@ function listenToWalletBackground(listener: (m: any) => void): () => void {
const loaderSettings = {
isAutoOpenEnabled: false,
isDomainTrusted: false,
-}
+};
function start(
onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => void) => void,
- onHeadReady: (listener: (el: HTMLHeadElement) => void) => void
+ onHeadReady: (listener: (el: HTMLHeadElement) => void) => void,
) {
// do not run everywhere, this is just expected to run on site
// that are aware of taler
if (shouldNotInject) return;
- const isAutoOpenEnabled_promise = callBackground("isAutoOpenEnabled", undefined).then(result => {
+ const isAutoOpenEnabled_promise = callBackground(
+ "isAutoOpenEnabled",
+ undefined,
+ ).then((result) => {
loaderSettings.isAutoOpenEnabled = result;
return result;
- })
+ });
const isDomainTrusted_promise = callBackground("isDomainTrusted", {
- domain: window.location.origin
- }).then(result => {
+ domain: window.location.origin,
+ }).then((result) => {
loaderSettings.isDomainTrusted = result;
return result;
- })
+ });
onTalerMetaTagFound(async (el) => {
await isAutoOpenEnabled_promise;
if (!loaderSettings.isAutoOpenEnabled) {
return;
}
- redirectToTalerActionHandler(el)
- })
+ redirectToTalerActionHandler(el);
+ });
onHeadReady(async (el) => {
- const trusted = await isDomainTrusted_promise
- injectTalerSupportScript(el, trusted)
- })
+ const trusted = await isDomainTrusted_promise;
+ injectTalerSupportScript(el, trusted);
+ });
listenToWalletBackground((e: MessageFromBackend) => {
- if (e.type === "web-extension" && e.notification.type === "settings-change") {
- const settings = e.notification.currentValue
- loaderSettings.isAutoOpenEnabled = settings.autoOpen
+ if (
+ e.type === "web-extension" &&
+ e.notification.type === "settings-change"
+ ) {
+ const settings = e.notification.currentValue;
+ loaderSettings.isAutoOpenEnabled = settings.autoOpen;
}
- })
-
+ });
}
function isCorrectMetaElement(el: HTMLMetaElement): boolean {
- const name = el.getAttribute("name")
+ const name = el.getAttribute("name");
if (!name) return false;
if (name !== "taler-uri") return false;
const uri = el.getAttribute("content");
if (!uri) return false;
- return true
+ return true;
}
/**
* Tries to find taler meta tag ASAP and report
- * @param notify
- * @returns
+ * @param notify
+ * @returns
*/
function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
if (document.head) {
- const element = document.head.querySelector("meta[name=taler-uri]")
+ const element = document.head.querySelector("meta[name=taler-uri]");
if (!element) return;
if (!(element instanceof HTMLMetaElement)) return;
if (isCorrectMetaElement(element)) {
- notify(element)
+ notify(element);
}
return;
}
@@ -315,34 +339,33 @@ function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLMetaElement) {
if (isCorrectMetaElement(added)) {
- notify(added)
- obs.disconnect()
+ notify(added);
+ obs.disconnect();
}
}
});
}
});
} catch (e) {
- console.error(e)
+ console.error(e);
}
- })
+ });
obs.observe(document, {
childList: true,
subtree: true,
attributes: false,
- })
-
+ });
}
/**
* Tries to find HEAD tag ASAP and report
- * @param notify
- * @returns
+ * @param notify
+ * @returns
*/
function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
if (document.head) {
- notify(document.head)
+ notify(document.head);
return;
}
const obs = new MutationObserver(async function (mutations) {
@@ -351,22 +374,22 @@ function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
if (mut.type === "childList") {
mut.addedNodes.forEach((added) => {
if (added instanceof HTMLHeadElement) {
- notify(added)
- obs.disconnect()
+ notify(added);
+ obs.disconnect();
}
});
}
});
} catch (e) {
- console.error(e)
+ console.error(e);
}
- })
+ });
obs.observe(document, {
childList: true,
subtree: true,
attributes: false,
- })
+ });
}
start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
index 8b15380f9..c6d96ba01 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -14,6 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { encodeCrockForURI } from "@gnu-taler/web-util/browser";
+
/**
* WARNING
*
@@ -22,10 +24,10 @@
*/
(() => {
const logger = {
- debug: (...msg: any[]) => { },
- info: (...msg: any[]) =>
+ debug: (..._msg: unknown[]) => { },
+ info: (...msg: unknown[]) =>
console.log(`${new Date().toISOString()} TALER`, ...msg),
- error: (...msg: any[]) =>
+ error: (...msg: unknown[]) =>
console.error(`${new Date().toISOString()} TALER`, ...msg),
};
@@ -76,13 +78,17 @@
return undefined;
}
const host = `${config.protocol}//${config.hostname}`;
- const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
+ // FIXME: mem leak problems
+ // const path = `static/wallet.html#/taler-uri/${encodeCrockForURI(uri)}`;
+ const path = `static/wallet.html#/taler-uri-simple/${encodeURIComponent(uri)}`;
return `${host}/${path}`;
}
function anchorOnClick(ev: MouseEvent) {
if (!(ev.currentTarget instanceof Element)) {
- logger.debug(`onclick: registered in a link that is not an HTML element`);
+ logger.debug(
+ `onclick: registered in a link that is not an HTML element`,
+ );
return;
}
const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
@@ -95,7 +101,9 @@
targetAttr && targetAttr.value ? targetAttr.value : "_self";
const page = convertURIToWebExtensionPath(hrefAttr.value);
if (!page) {
- logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
+ logger.debug(
+ `onclick: could not convert "${hrefAttr.value}" into path`,
+ );
return;
}
// we can use window.open, but maybe some browser will block it?
@@ -118,7 +126,7 @@
function checkForNewAnchors(
mutations: MutationRecord[],
- observer: MutationObserver,
+ _observer: MutationObserver,
) {
mutations.forEach((mut) => {
if (mut.type === "childList") {
@@ -137,7 +145,7 @@
* Register the anchor handler when found
*/
function registerProtocolHandler() {
- if (document.body) overrideAllAnchor(document.body)
+ if (document.body) overrideAllAnchor(document.body);
new MutationObserver(checkForNewAnchors).observe(document, {
childList: true,
subtree: true,
@@ -179,7 +187,8 @@
};
if (apiEnabled) {
- //@ts-ignore
+ // @ts-expect-error we now that `taler` doesn't exist.
+ // we are creating the property
window.taler = taler;
}
@@ -196,5 +205,4 @@
}
start();
-})()
-
+})();
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
index 94b32c157..43898ecc1 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
+import { OperationAlternative, OperationFail, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
@@ -34,13 +34,6 @@ export type State = State.Loading
| State.Confirm
| State.Verify;
-export type CheckExchangeErrors = {
- "invalid-version": string;
- "invalid-currency": string;
- "not-found": void;
- "already-active": void;
- "invalid-protocol": void;
-}
export namespace State {
export interface Loading {
@@ -73,11 +66,16 @@ export namespace State {
url: TextFieldHandler,
loading: boolean;
knownExchanges: URL[],
- result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> | OperationFailWithBody<CheckExchangeErrors> | undefined,
+ result: OperationOk<TalerExchangeApi.ExchangeKeysResponse>
+ | OperationAlternative<"invalid-version", string>
+ | OperationAlternative<"invalid-currency", string>
+ | OperationFail<"not-found">
+ | OperationFail<"already-active">
+ | OperationFail<"invalid-protocol">
+ | undefined,
expectedCurrency: string | undefined,
}
}
-
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
index 4a04f762a..c0756d1e2 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
+import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailure, opKnownFailureWithBody } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
import { useCallback, useEffect, useState } from "preact/hooks";
@@ -22,7 +22,7 @@ import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { withSafe } from "../../mui/handlers.js";
import { RecursiveState } from "../../utils/index.js";
-import { CheckExchangeErrors, Props, State } from "./index.js";
+import { Props, State } from "./index.js";
function urlFromInput(str: string): URL {
let result: URL;
@@ -59,11 +59,11 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) {
const baseUrl = urlFromInput(str)
if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
- return opKnownFailureWithBody<CheckExchangeErrors>("invalid-protocol", undefined)
+ return opKnownFailure("invalid-protocol"as const)
}
const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href);
if (found !== -1) {
- return opKnownFailureWithBody<CheckExchangeErrors>("already-active", undefined);
+ return opKnownFailure("already-active"as const);
}
/**
@@ -84,13 +84,13 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any);
const config = await api.getConfig()
if (config.type === "fail") {
- return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined)
+ return opKnownFailure("not-found" as const)
}
if (!api.isCompatible(config.body.version)) {
- return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
+ return opKnownFailureWithBody("invalid-version"as const, config.body.version)
}
if (currency !== undefined && currency !== config.body.currency) {
- return opKnownFailureWithBody<CheckExchangeErrors>("invalid-currency", config.body.currency)
+ return opKnownFailureWithBody("invalid-currency"as const, config.body.currency)
}
const keys = await api.getKeys()
return keys
@@ -177,7 +177,7 @@ function useDebounce<T>(
setError(er);
} else {
// @ts-expect-error cause still not in typescript
- setError(new Error('unkown error on debounce', { cause: er }))
+ setError(new Error('unknown error on debounce', { cause: er }))
}
setLoading(false);
setResult(undefined);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
index f6537bc68..882d95670 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -120,8 +120,9 @@ export function VerifyView({
</WarningBox>
);
}
+
default: {
- assertUnreachable(result.case);
+ assertUnreachable(result);
}
}
})()}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
index dd1777fd1..62f1ffbb1 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
@@ -27,7 +27,7 @@ export interface Props {
export function AddNewActionView({ onCancel }: Props): VNode {
const [url, setUrl] = useState("");
- const uri = parseTalerUri(url);
+ const uri = parseTalerUri(url.toLowerCase());
const { i18n } = useTranslationContext();
async function redirectToWallet(): Promise<void> {
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 893122c0f..474f5acdb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -22,14 +22,21 @@
import {
Amounts,
+ ScopeInfo,
TalerUri,
TalerUriAction,
TranslatedString,
+ parsePaytoUri,
+ parseScopeInfoShort,
parseTalerUri,
+ stringifyPaytoUri,
+ stringifyScopeInfoShort,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
import {
TranslationProvider,
+ decodeCrockFromURI,
+ encodeCrockForURI,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
@@ -87,6 +94,8 @@ import { WalletActivity } from "../components/WalletActivity.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { DevExperimentPage } from "../cta/DevExperiment/index.js";
import { ConfirmAddExchangeView } from "./AddExchange/views.js";
+import { ManageAccountPage } from "./ManageAccount/index.js";
+import { SupportedBanksForAccount } from "./SupportedBanksForAccount.js";
export function Application(): VNode {
const { i18n } = useTranslationContext();
@@ -96,7 +105,7 @@ export function Application(): VNode {
redirectTo(Pages.balanceTransaction({ tid }));
}
function redirectToURL(str: string): void {
- window.location.href = new URL(str).href
+ window.location.href = new URL(str).href;
}
return (
@@ -115,12 +124,17 @@ export function Application(): VNode {
<Route
path={Pages.qr}
component={() => (
- <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <WalletTemplate
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<QrReaderPage
onDetected={(talerActionUrl: TalerUri) => {
redirectTo(
Pages.defaultCta({
- uri: stringifyTalerUri(talerActionUrl),
+ uri: encodeCrockForURI(
+ stringifyTalerUri(talerActionUrl),
+ ),
}),
);
}}
@@ -132,7 +146,10 @@ export function Application(): VNode {
<Route
path={Pages.settings}
component={() => (
- <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <WalletTemplate
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<SettingsPage />
</WalletTemplate>
)}
@@ -159,17 +176,33 @@ export function Application(): VNode {
<Route
path={Pages.balanceHistory.pattern}
- component={({ currency }: { currency?: string }) => (
- <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ component={({ scope }: { scope?: string }) => (
+ <WalletTemplate
+ path="balance"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<HistoryPage
- currency={currency}
- goToWalletDeposit={(currency: string) =>
- redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ scope={
+ !scope
+ ? undefined
+ : parseScopeInfoShort(decodeCrockFromURI(scope))
}
- goToWalletManualWithdraw={(currency?: string) =>
+ goToWalletDeposit={(scope: ScopeInfo) =>
+ redirectTo(
+ Pages.sendCash({
+ scope: encodeCrockForURI(
+ stringifyScopeInfoShort(scope),
+ ),
+ }),
+ )
+ }
+ goToWalletManualWithdraw={(scope?: ScopeInfo) =>
redirectTo(
Pages.receiveCash({
- amount: !currency ? undefined : `${currency}:0`,
+ scope: !scope
+ ? undefined
+ : encodeCrockForURI(stringifyScopeInfoShort(scope)),
}),
)
}
@@ -179,18 +212,34 @@ export function Application(): VNode {
/>
<Route
path={Pages.searchHistory.pattern}
- component={({ currency }: { currency?: string }) => (
- <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ component={({ scope }: { scope?: string }) => (
+ <WalletTemplate
+ path="balance"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<HistoryPage
- currency={currency}
+ scope={
+ !scope
+ ? undefined
+ : parseScopeInfoShort(decodeCrockFromURI(scope))
+ }
search
- goToWalletDeposit={(currency: string) =>
- redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ goToWalletDeposit={(scope: ScopeInfo) =>
+ redirectTo(
+ Pages.sendCash({
+ scope: encodeCrockForURI(
+ stringifyScopeInfoShort(scope),
+ ),
+ }),
+ )
}
- goToWalletManualWithdraw={(currency?: string) =>
+ goToWalletManualWithdraw={(scope?: ScopeInfo) =>
redirectTo(
Pages.receiveCash({
- amount: !currency ? undefined : `${currency}:0`,
+ scope: !scope
+ ? undefined
+ : encodeCrockForURI(stringifyScopeInfoShort(scope)),
}),
)
}
@@ -200,37 +249,127 @@ export function Application(): VNode {
/>
<Route
path={Pages.sendCash.pattern}
- component={({ amount }: { amount?: string }) => (
- <WalletTemplate path="balance" goToURL={redirectToURL}>
- <DestinationSelectionPage
- type="send"
- amount={amount}
- goToWalletBankDeposit={(amount: string) =>
- redirectTo(Pages.balanceDeposit({ amount }))
- }
- goToWalletWalletSend={(amount: string) =>
- redirectTo(Pages.ctaTransferCreate({ amount }))
- }
- />
- </WalletTemplate>
- )}
+ component={({ scope }: { scope?: string }) => {
+ if (!scope) return <Redirect to={Pages.balanceHistory({})} />;
+ const s = parseScopeInfoShort(decodeCrockFromURI(scope));
+ if (!s) return <Redirect to={Pages.balanceHistory({})} />;
+
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="send"
+ scope={s}
+ goToWalletKnownBankDeposit={(s, p) =>
+ redirectTo(
+ Pages.ctaDeposit({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ account: encodeCrockForURI(stringifyPaytoUri(p)),
+ }),
+ )
+ }
+ goToWalletNewBankDeposit={(s) =>
+ redirectTo(
+ Pages.bankManange({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ }),
+ )
+ }
+ goToWalletWalletSend={(s) =>
+ redirectTo(
+ Pages.ctaTransferCreate({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ );
+ }}
+ />
+ <Route
+ path={Pages.bankManange.pattern}
+ component={({ scope }: { scope?: string }) => {
+ const s = !scope
+ ? undefined
+ : parseScopeInfoShort(decodeCrockFromURI(scope));
+ if (!s) return <div>missing scope</div>;
+
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <ManageAccountPage
+ scope={s}
+ onAccountAdded={(account) =>
+ redirectTo(
+ Pages.ctaDeposit({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ account: encodeCrockForURI(
+ stringifyPaytoUri(account),
+ ),
+ }),
+ )
+ }
+ onCancel={() => {
+ redirectTo(
+ Pages.balanceHistory({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ }),
+ );
+ }}
+ />
+ </WalletTemplate>
+ );
+ }}
+ />
+ <Route
+ path={Pages.receiveCashForPurchase.pattern}
+ component={({ id: _purchaseId }: { id?: string }) => {
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ not yet implemented
+ </WalletTemplate>
+ );
+ }}
+ />
+ <Route
+ path={Pages.receiveCashForInvoice.pattern}
+ component={({ id: _invoiceId }: { id?: string }) => {
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ not yet implemented
+ </WalletTemplate>
+ );
+ }}
/>
<Route
path={Pages.receiveCash.pattern}
- component={({ amount }: { amount?: string }) => (
- <WalletTemplate path="balance" goToURL={redirectToURL}>
- <DestinationSelectionPage
- type="get"
- amount={amount}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.ctaWithdrawManual({ amount }))
- }
- goToWalletWalletInvoice={(amount?: string) =>
- redirectTo(Pages.ctaInvoiceCreate({ amount }))
- }
- />
- </WalletTemplate>
- )}
+ component={({ scope }: { scope?: string }) => {
+ const s = !scope
+ ? undefined
+ : parseScopeInfoShort(decodeCrockFromURI(scope));
+
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="get"
+ scope={s}
+ goToWalletManualWithdraw={(s) =>
+ redirectTo(
+ Pages.ctaWithdrawManual({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ }),
+ )
+ }
+ goToWalletWalletInvoice={(s) =>
+ redirectTo(
+ Pages.ctaInvoiceCreate({
+ scope: encodeCrockForURI(stringifyScopeInfoShort(s)),
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ );
+ }}
/>
<Route
@@ -239,8 +378,14 @@ export function Application(): VNode {
<WalletTemplate path="balance" goToURL={redirectToURL}>
<TransactionPage
tid={tid}
- goToWalletHistory={(currency?: string) =>
- redirectTo(Pages.balanceHistory({ currency }))
+ goToWalletHistory={(scope: ScopeInfo) =>
+ redirectTo(
+ Pages.balanceHistory({
+ scope: encodeCrockForURI(
+ stringifyScopeInfoShort(scope),
+ ),
+ }),
+ )
}
/>
</WalletTemplate>
@@ -249,25 +394,47 @@ export function Application(): VNode {
<Route
path={Pages.balanceDeposit.pattern}
- component={({ amount }: { amount: string }) => (
- <WalletTemplate path="balance" goToURL={redirectToURL}>
- <DepositPage
- amount={amount}
- onCancel={(currency: string) => {
- redirectTo(Pages.balanceHistory({ currency }));
- }}
- onSuccess={(currency: string) => {
- redirectTo(Pages.balanceHistory({ currency }));
- }}
- />
- </WalletTemplate>
- )}
+ component={({ scope }: { scope: string }) => {
+ const s = parseScopeInfoShort(decodeCrockFromURI(scope));
+ if (!s) {
+ return <div>missing scope</div>;
+ }
+ return (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DepositPage
+ scope={s}
+ onCancel={(scope: ScopeInfo) => {
+ redirectTo(
+ Pages.balanceHistory({
+ scope: encodeCrockForURI(
+ stringifyScopeInfoShort(scope),
+ ),
+ }),
+ );
+ }}
+ onSuccess={(scope: ScopeInfo) => {
+ redirectTo(
+ Pages.balanceHistory({
+ scope: encodeCrockForURI(
+ stringifyScopeInfoShort(scope),
+ ),
+ }),
+ );
+ }}
+ />
+ </WalletTemplate>
+ );
+ }}
/>
<Route
path={Pages.backup}
component={() => (
- <WalletTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <WalletTemplate
+ path="backup"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<BackupPage
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
@@ -283,8 +450,9 @@ export function Application(): VNode {
onPayProvider={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
- onWithdraw={(amount: string) =>
- redirectTo(Pages.receiveCash({ amount }))
+ onWithdraw={(_amount: string) =>
+ // FIXME: use receiveCashForPurchase
+ redirectTo(Pages.receiveCash({ scope: "FIXME missing" }))
}
onBack={() => redirectTo(Pages.backup)}
/>
@@ -314,7 +482,11 @@ export function Application(): VNode {
<Route
path={Pages.dev}
component={() => (
- <WalletTemplate path="dev" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <WalletTemplate
+ path="dev"
+ goToTransaction={redirectToTxInfo}
+ goToURL={redirectToURL}
+ >
<DeveloperPage />
</WalletTemplate>
)}
@@ -326,7 +498,7 @@ export function Application(): VNode {
<Route
path={Pages.defaultCta.pattern}
component={({ uri }: { uri: string }) => {
- const path = getPathnameForTalerURI(uri);
+ const path = getPathnameForTalerURI(decodeCrockFromURI(uri));
if (!path) {
return (
<CallToActionTemplate title={i18n.str`Taler URI handler`}>
@@ -343,14 +515,37 @@ export function Application(): VNode {
return <Redirect to={path} />;
}}
/>
+ {/* // FIXME: mem leak problems */}
+ <Route
+ path={Pages.defaultCtaSimple.pattern}
+ component={({ uri }: { uri: string }) => {
+ const path = getPathnameForTalerURI(decodeURIComponent(uri));
+ if (!path) {
+ return (
+ <CallToActionTemplate title={i18n.str`Taler URI handler`}>
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Could not found a handler for the Taler URI`,
+ description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
+ }}
+ />
+ </CallToActionTemplate>
+ );
+ }
+ return <Redirect to={path} />;
+ }}
+ />
+
<Route
path={Pages.ctaPay}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentPage
- talerPayUri={decodeURIComponent(talerUri)}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.receiveCash({ amount }))
+ talerPayUri={decodeCrockFromURI(talerUri)}
+ goToWalletManualWithdraw={(_amount?: string) =>
+ // FIXME: use receiveCashForPruchase
+ redirectTo(Pages.receiveCash({}))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
@@ -365,9 +560,10 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentTemplatePage
- talerTemplateUri={decodeURIComponent(talerUri)}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.receiveCash({ amount }))
+ talerTemplateUri={decodeCrockFromURI(talerUri)}
+ goToWalletManualWithdraw={(_amount?: string) =>
+ // FIXME: use receiveCashForPruchase
+ redirectTo(Pages.receiveCash({}))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
@@ -382,7 +578,7 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash refund`}>
<RefundPage
- talerRefundUri={decodeURIComponent(talerUri)}
+ talerRefundUri={decodeCrockFromURI(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@@ -396,7 +592,7 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromURI
- talerWithdrawUri={decodeURIComponent(talerUri)}
+ talerWithdrawUri={decodeCrockFromURI(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@@ -408,55 +604,71 @@ export function Application(): VNode {
<Route
path={Pages.ctaWithdrawManual.pattern}
component={({
+ scope,
amount,
talerUri,
}: {
+ scope: string;
amount: string;
talerUri: string;
- }) => (
- <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
- <WithdrawPageFromParams
- onAmountChanged={async (newamount) => {
- const page = `${Pages.ctaWithdrawManual({ amount: newamount })}?talerUri=${encodeURIComponent(talerUri)}`;
- redirectTo(page);
- }}
- talerExchangeWithdrawUri={talerUri}
- amount={amount}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- </CallToActionTemplate>
- )}
+ }) => {
+ if (!scope) return <Redirect to={Pages.balanceHistory({})} />;
+ const s = parseScopeInfoShort(decodeCrockFromURI(scope));
+ if (!s) return <Redirect to={Pages.balanceHistory({})} />;
+
+ return (
+ <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
+ <WithdrawPageFromParams
+ scope={s}
+ talerExchangeWithdrawUri={talerUri}
+ amount={amount}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ );
+ }}
/>
<Route
- path={Pages.ctaDeposit}
+ path={Pages.ctaDeposit.pattern}
component={({
- amount,
- talerUri,
+ scope,
+ account,
}: {
- amount: string;
- talerUri: string;
- }) => (
- <CallToActionTemplate title={i18n.str`Digital cash deposit`}>
- <DepositPageCTA
- amountStr={Amounts.stringify(Amounts.parseOrThrow(amount))}
- talerDepositUri={decodeURIComponent(talerUri)}
- cancel={() => redirectTo(Pages.balance)}
- onSuccess={(tid: string) =>
- redirectTo(Pages.balanceTransaction({ tid }))
- }
- />
- </CallToActionTemplate>
- )}
+ scope: string;
+ account: string;
+ }) => {
+ const s = parseScopeInfoShort(decodeCrockFromURI(scope));
+ if (!s) {
+ return <div>missing scope</div>;
+ }
+ const p = parsePaytoUri(decodeCrockFromURI(account));
+ if (!p) {
+ return <div>missing account</div>;
+ }
+
+ return (
+ <CallToActionTemplate title={i18n.str`Digital cash deposit`}>
+ <DepositPageCTA
+ scope={s}
+ account={p}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ );
+ }}
/>
<Route
path={Pages.ctaInvoiceCreate.pattern}
- component={({ amount }: { amount: string }) => (
+ component={({ scope }: { scope: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoiceCreatePage
- amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ scope={parseScopeInfoShort(decodeCrockFromURI(scope))!}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@@ -467,10 +679,10 @@ export function Application(): VNode {
/>
<Route
path={Pages.ctaTransferCreate.pattern}
- component={({ amount }: { amount: string }) => (
+ component={({ scope }: { scope: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferCreatePage
- amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ scope={parseScopeInfoShort(decodeCrockFromURI(scope))!}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@@ -484,9 +696,10 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoicePayPage
- talerPayPullUri={decodeURIComponent(talerUri)}
- goToWalletManualWithdraw={(amount?: string) =>
- redirectTo(Pages.receiveCash({ amount }))
+ talerPayPullUri={decodeCrockFromURI(talerUri)}
+ goToWalletManualWithdraw={(_amount?: string) =>
+ // FIXME: use receiveCashForInvoice
+ redirectTo(Pages.receiveCash({}))
}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
@@ -501,7 +714,7 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferPickupPage
- talerPayPushUri={decodeURIComponent(talerUri)}
+ talerPayPushUri={decodeCrockFromURI(talerUri)}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@@ -515,7 +728,7 @@ export function Application(): VNode {
component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash recovery`}>
<RecoveryPage
- talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
+ talerRecoveryUri={decodeCrockFromURI(talerRecoveryUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={() => redirectTo(Pages.backup)}
/>
@@ -527,7 +740,7 @@ export function Application(): VNode {
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Development experiment`}>
<DevExperimentPage
- talerExperimentUri={decodeURIComponent(talerUri)}
+ talerExperimentUri={decodeCrockFromURI(talerUri)}
onCancel={() => redirectTo(Pages.balanceHistory({}))}
onSuccess={() => redirectTo(Pages.balanceHistory({}))}
/>
@@ -537,23 +750,63 @@ export function Application(): VNode {
<Route
path={Pages.ctaAddExchange}
component={({ talerUri }: { talerUri: string }) => {
- const tUri = parseTalerUri(decodeURIComponent(talerUri))
- const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined
+ const tUri = parseTalerUri(
+ decodeCrockFromURI(talerUri).toLowerCase(),
+ );
+ const baseUrl =
+ tUri?.type === TalerUriAction.AddExchange
+ ? tUri.exchangeBaseUrl
+ : undefined;
if (!baseUrl) {
- redirectTo(Pages.balanceHistory({}))
- return <div>
- invalid url {talerUri}
- </div>
+ redirectTo(Pages.balanceHistory({}));
+ return <div>invalid url {talerUri}</div>;
}
- return <CallToActionTemplate title={i18n.str`Add exchange`}>
- <ConfirmAddExchangeView
- url={baseUrl}
- status="confirm"
- error={undefined}
- onCancel={() => redirectTo(Pages.balanceHistory({}))}
- onConfirm={() => redirectTo(Pages.balanceHistory({}))}
- />
- </CallToActionTemplate>
+ return (
+ <CallToActionTemplate title={i18n.str`Add exchange`}>
+ <ConfirmAddExchangeView
+ url={baseUrl}
+ status="confirm"
+ error={undefined}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onConfirm={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ );
+ }}
+ />
+ <Route
+ path={Pages.paytoBanks.pattern}
+ component={({ payto }: { payto: string }) => {
+ const pUri = parsePaytoUri(
+ decodeCrockFromURI(payto).toLowerCase(),
+ );
+ if (!pUri) {
+ redirectTo(Pages.balanceHistory({}));
+ return <div>invalid uri {pUri}</div>;
+ }
+ return (
+ <WalletTemplate goToURL={redirectToURL}>
+ <SupportedBanksForAccount account={pUri} />
+ </WalletTemplate>
+ );
+ }}
+ />
+ <Route
+ path={Pages.paytoQrs.pattern}
+ component={({ payto }: { payto: string }) => {
+ const pUri = parsePaytoUri(
+ decodeCrockFromURI(payto).toLowerCase(),
+ );
+ if (!pUri) {
+ redirectTo(Pages.balanceHistory({}));
+ return <div>invalid uri {pUri}</div>;
+ }
+ return (
+ <WalletTemplate goToURL={redirectToURL}>
+ {/* <AllQrsForAccount account={pUri} /> */}
+ <pre>{JSON.stringify({ title: "QRS", pUri })}</pre>
+ </WalletTemplate>
+ );
}}
/>
{/**
@@ -613,7 +866,7 @@ function CallToActionTemplate({
<WalletAction>
<LogoHeader />
<section style={{ display: "flex", justifyContent: "right", margin: 0 }}>
- <LinkPrimary href={Pages.balance}>
+ <LinkPrimary href={`#${Pages.balance}`}>
<div
style={{
height: 24,
@@ -633,7 +886,7 @@ function CallToActionTemplate({
{children}
</AlertProvider>
<section style={{ display: "flex", justifyContent: "right" }}>
- <LinkPrimary href={Pages.balance}>
+ <LinkPrimary href={`#${Pages.balance}`}>
<i18n.Translate>Return to wallet</i18n.Translate>
</LinkPrimary>
</section>
@@ -665,7 +918,8 @@ function WalletTemplate({
<WalletNavBar path={path} />
<PendingTransactions
goToTransaction={goToTransaction}
- goToURL={goToURL} />
+ goToURL={goToURL}
+ />
<WalletBox>
<AlertProvider>
<CurrentAlerts />
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index 8a3710f69..645fbf67c 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -261,9 +261,9 @@ function BackupLayout(props: TransactionLayoutProps): VNode {
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
<a
- href={Pages.backupProviderDetail({
- pid: encodeURIComponent(props.id),
- })}
+ href={`#${Pages.backupProviderDetail({
+ pid: props.id,
+ })}`}
>
<span>{props.title}</span>
</a>
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
index daba6aba4..22ad1c1e7 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, PaytoUri } from "@gnu-taler/taler-util";
+import { AmountJson, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
@@ -34,9 +34,9 @@ import {
} from "./views.js";
export interface Props {
- amount?: string;
- onCancel: (currency: string) => void;
- onSuccess: (currency: string) => void;
+ scope:ScopeInfo;
+ onCancel: (scope: ScopeInfo) => void;
+ onSuccess: (scope: ScopeInfo) => void;
}
export type State =
@@ -62,8 +62,8 @@ export namespace State {
export interface AddingAccount {
status: "manage-account";
error: undefined;
- currency: string;
- onAccountAdded: (p: string) => void;
+ scope: ScopeInfo;
+ onAccountAdded: (p: PaytoUri) => void;
onCancel: () => void;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
index b674665cf..29f533385 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -32,47 +32,35 @@ import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";
export function useComponentState({
- amount: amountStr,
+ scope,
onCancel,
onSuccess,
}: Props): RecursiveState<State> {
const api = useBackendContext();
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
- const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr);
- const currency = parsed !== undefined ? parsed.currency : undefined;
+
+ const zero = Amounts.zeroOfCurrency(scope.currency);
const hook = useAsyncAsHook(async () => {
const { balances } = await api.wallet.call(
WalletApiOperation.GetBalances,
- {},
+ {
+ },
);
+
const { accounts } = await api.wallet.call(
WalletApiOperation.ListKnownBankAccounts,
- { currency },
+ { currency: scope.currency },
);
return { accounts, balances };
});
- const initialValue =
- parsed !== undefined
- ? parsed
- : currency !== undefined
- ? Amounts.zeroOfCurrency(currency)
- : undefined;
- // const [accountIdx, setAccountIdx] = useState<number>(0);
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
const [addingAccount, setAddingAccount] = useState(false);
- if (!currency) {
- return {
- status: "amount-or-currency-error",
- error: undefined,
- };
- }
-
if (!hook) {
return {
status: "loading",
@@ -102,9 +90,9 @@ export function useComponentState({
return {
status: "manage-account",
error: undefined,
- currency,
- onAccountAdded: (p: string) => {
- updateAccountFromList(p);
+ scope,
+ onAccountAdded: (p: PaytoUri) => {
+ updateAccountFromList(stringifyPaytoUri(p));
setAddingAccount(false);
hook.retry();
},
@@ -115,17 +103,17 @@ export function useComponentState({
};
}
- const bs = balances.filter((b) => b.available.startsWith(currency));
+ const bs = balances.filter((b) => b.scopeInfo === scope);
const balance =
bs.length > 0
? Amounts.parseOrThrow(bs[0].available)
- : Amounts.zeroOfCurrency(currency);
+ : Amounts.zeroOfCurrency(scope.currency);
if (Amounts.isZero(balance)) {
return {
status: "no-enough-balance",
error: undefined,
- currency,
+ currency: scope.currency,
};
}
@@ -133,7 +121,7 @@ export function useComponentState({
return {
status: "no-accounts",
error: undefined,
- currency,
+ currency: scope.currency,
onAddAccount: {
onClick: pushAlertOnError(async () => {
setAddingAccount(true);
@@ -143,10 +131,9 @@ export function useComponentState({
}
const firstAccount = accounts[0].uri;
const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
- const zero = Amounts.zeroOfCurrency(currency)
return (): State => {
const [instructed, setInstructed] = useState(
- {amount: initialValue ?? zero, type: TransactionAmountMode.Raw},
+ { amount: zero, type: TransactionAmountMode.Raw },
);
const amountStr = Amounts.stringify(instructed.amount);
const depositPaytoUri = stringifyPaytoUri(currentAccount);
@@ -188,12 +175,12 @@ export function useComponentState({
const totalFee =
fee !== undefined
? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount
- : Amounts.zeroOfCurrency(currency);
+ : zero;
const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount);
const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount);
- const isDirty = instructed.amount !== initialValue;
+ const isDirty = instructed.amount !== zero;
const amountError = !isDirty
? undefined
: Amounts.cmp(balance, totalEffective) === -1
@@ -206,7 +193,7 @@ export function useComponentState({
amountError !== undefined; //amount field may be invalid
async function doSend(): Promise<void> {
- if (!currency) return;
+ // if (!currency) return;
const depositPaytoUri = stringifyPaytoUri(currentAccount);
const amountStr = Amounts.stringify(totalEffective);
@@ -214,13 +201,13 @@ export function useComponentState({
amount: amountStr,
depositPaytoUri,
});
- onSuccess(currency);
+ onSuccess(scope!);
}
return {
status: "ready",
error: undefined,
- currency,
+ currency: scope.currency,
amount: {
value: totalEffective,
onInput: pushAlertOnError(async (a) => setInstructed({
@@ -250,7 +237,7 @@ export function useComponentState({
currentAccount,
cancelHandler: {
onClick: pushAlertOnError(async () => {
- onCancel(currency);
+ onCancel(scope!);
}),
},
depositHandler: {
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
index 1144095e1..a96f09553 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
@@ -24,6 +24,7 @@ import {
Amounts,
AmountString,
parsePaytoUri,
+ ScopeInfo,
ScopeType,
stringifyPaytoUri
} from "@gnu-taler/taler-util";
@@ -36,21 +37,26 @@ import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
const currency = "EUR";
-const amount = `${currency}:0`;
+const amount = Amounts.parseOrThrow(`${currency}:0`);
const withoutFee = (value: number): AmountResponse => ({
effectiveAmount: `${currency}:${value}` as AmountString,
rawAmount: `${currency}:${value}` as AmountString,
});
+const defaultScope: ScopeInfo = {
+ type: ScopeType.Global,
+ currency
+}
+
+
const withSomeFee = (value: number, fee: number): AmountResponse => ({
effectiveAmount: `${currency}:${value}` as AmountString,
rawAmount: `${currency}:${value - fee}` as AmountString,
});
-
describe("DepositPage states", () => {
it("should have status 'no-enough-balance' when balance is empty", async () => {
const { handler, TestingContext } = createWalletApiMock();
- const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+ const props = { scope: defaultScope, amount, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
@@ -61,11 +67,7 @@ describe("DepositPage states", () => {
pendingIncoming: `${currency}:0` as AmountString,
pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
- scopeInfo: {
- currency,
- type: ScopeType.Auditor,
- url: "asd",
- },
+ scopeInfo: defaultScope,
},
],
});
@@ -97,7 +99,7 @@ describe("DepositPage states", () => {
it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => {
const { handler, TestingContext } = createWalletApiMock();
- const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+ const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
@@ -108,11 +110,7 @@ describe("DepositPage states", () => {
pendingIncoming: `${currency}:0` as AmountString,
pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
- scopeInfo: {
- currency,
- type: ScopeType.Auditor,
- url: "asd",
- },
+ scopeInfo: defaultScope,
},
],
});
@@ -157,7 +155,7 @@ describe("DepositPage states", () => {
it("should have status 'ready' but unable to deposit ", async () => {
const { handler, TestingContext } = createWalletApiMock();
- const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+ const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
@@ -168,11 +166,7 @@ describe("DepositPage states", () => {
pendingIncoming: `${currency}:0` as AmountString,
pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
- scopeInfo: {
- currency,
- type: ScopeType.Auditor,
- url: "asd",
- },
+ scopeInfo: defaultScope,
},
],
});
@@ -217,7 +211,7 @@ describe("DepositPage states", () => {
it("should not be able to deposit more than the balance ", async () => {
const { handler, TestingContext } = createWalletApiMock();
- const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+ const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
@@ -228,11 +222,7 @@ describe("DepositPage states", () => {
pendingIncoming: `${currency}:0` as AmountString,
pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
- scopeInfo: {
- currency,
- type: ScopeType.Auditor,
- url: "asd",
- },
+ scopeInfo: defaultScope,
},
],
});
@@ -307,7 +297,7 @@ describe("DepositPage states", () => {
it("should calculate the fee upon entering amount ", async () => {
const { handler, TestingContext } = createWalletApiMock();
- const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+ const props = { scope: defaultScope, onCancel: nullFunction, onSuccess: nullFunction };
handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
balances: [
@@ -318,11 +308,7 @@ describe("DepositPage states", () => {
pendingIncoming: `${currency}:0` as AmountString,
pendingOutgoing: `${currency}:0` as AmountString,
requiresUserInput: false,
- scopeInfo: {
- currency,
- type: ScopeType.Auditor,
- url: "asd",
- },
+ scopeInfo: defaultScope,
},
],
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
index b56fe5523..eeb972c08 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
@@ -14,15 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { AmountJson, KnownBankAccountsInfo, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import {
AmountFieldHandler,
- ButtonHandler,
- ToggleHandler,
+ ButtonHandler
} from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
import { ReadyView, SelectCurrencyView } from "./views.js";
@@ -30,15 +30,16 @@ export type Props = PropsGet | PropsSend;
interface PropsGet {
type: "get";
- amount?: string;
- goToWalletManualWithdraw: (amount: string) => void;
- goToWalletWalletInvoice: (amount: string) => void;
+ scope?: ScopeInfo;
+ goToWalletManualWithdraw: (s:ScopeInfo) => void;
+ goToWalletWalletInvoice: (s:ScopeInfo) => void;
}
interface PropsSend {
type: "send";
- amount?: string;
- goToWalletBankDeposit: (amount: string) => void;
- goToWalletWalletSend: (amount: string) => void;
+ scope: ScopeInfo;
+ goToWalletKnownBankDeposit: (s:ScopeInfo, p: PaytoUri) => void;
+ goToWalletNewBankDeposit: (s:ScopeInfo) => void;
+ goToWalletWalletSend: (s:ScopeInfo) => void;
}
export type State =
@@ -69,20 +70,13 @@ export namespace State {
status: "ready";
error: undefined;
type: Props["type"];
- selectCurrency: ButtonHandler;
- selectMax: ButtonHandler;
- previous: Contact[];
+ onSelectAccount: (p:PaytoUri) => void;
+ previous: KnownBankAccountsInfo[];
goToBank: ButtonHandler;
goToWallet: ButtonHandler;
- amountHandler: AmountFieldHandler;
}
}
-export type Contact = {
- icon_type: string;
- name: string;
- description: string;
-};
const viewMapping: StateViewMap<State> = {
loading: Loading,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
index d4e270a6c..de2d439b6 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -14,7 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import {
+ ExchangeUpdateStatus,
+ KnownBankAccountsInfo,
+ PaytoUri,
+ ScopeType,
+ parseScopeInfoShort,
+ stringifyScopeInfoShort
+} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
@@ -22,61 +29,46 @@ import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { RecursiveState, assertUnreachable } from "../../utils/index.js";
-import { Contact, Props, State } from "./index.js";
+import { Props, State } from "./index.js";
export function useComponentState(props: Props): RecursiveState<State> {
const api = useBackendContext();
const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
- const parsedInitialAmount = !props.amount
- ? undefined
- : Amounts.parse(props.amount);
+ const [scope, setScope] = useState(props.scope);
const hook = useAsyncAsHook(async () => {
- if (!parsedInitialAmount) return undefined;
- const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, {
- currency: parsedInitialAmount.currency,
- });
- return { balance };
+ const resp = await api.wallet.call(
+ WalletApiOperation.ListKnownBankAccounts,
+ {},
+ );
+ return resp
});
- const info = hook && !hook.hasError ? hook.response : undefined;
+ const previous: KnownBankAccountsInfo[] = props.type === "send" && hook && !hook.hasError ? hook.response.accounts : [];
- // const initialCurrency = parsedInitialAmount?.currency;
-
- const [amount, setAmount] = useState(
- !parsedInitialAmount ? undefined : parsedInitialAmount,
- );
- //FIXME: get this information from wallet
- // eslint-disable-next-line no-constant-condition
- const previous: Contact[] = true
- ? []
- : [
- {
- name: "International Bank",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- {
- name: "Max",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- {
- name: "Alex",
- icon_type: "bank",
- description: "account ending with 3454",
- },
- ];
-
- if (!amount) {
+ if (!scope) {
return () => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
const { i18n } = useTranslationContext();
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const hook = useAsyncAsHook(() =>
- api.wallet.call(WalletApiOperation.ListExchanges, {}),
- );
+ const hook = useAsyncAsHook(async () => {
+ const resp = await api.wallet.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ const unknownIndex = resp.exchanges.findIndex(
+ (d) => d.exchangeUpdateStatus === ExchangeUpdateStatus.Initial,
+ );
+ if (unknownIndex === -1) return resp;
+
+ await api.wallet.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: resp.exchanges[unknownIndex].exchangeBaseUrl,
+ force: true,
+ });
+
+ return await api.wallet.call(WalletApiOperation.ListExchanges, {});
+ });
if (!hook) {
return {
@@ -87,14 +79,30 @@ export function useComponentState(props: Props): RecursiveState<State> {
if (hook.hasError) {
return {
status: "error",
- error: alertFromError(i18n,
- i18n.str`Could not load exchanges`, hook),
+ error: alertFromError(i18n, i18n.str`Could not load exchanges`, hook),
};
}
const currencies: Record<string, string> = {};
- hook.response.exchanges.forEach((e) => {
- if (e.currency) {
- currencies[e.currency] = e.currency;
+ hook.response.exchanges.forEach((b) => {
+ switch (b.scopeInfo.type) {
+ case ScopeType.Global: {
+ currencies[stringifyScopeInfoShort(b.scopeInfo)] =
+ b.scopeInfo.currency;
+ break;
+ }
+ case ScopeType.Exchange: {
+ currencies[stringifyScopeInfoShort(b.scopeInfo)] =
+ `${b.scopeInfo.currency} ${b.scopeInfo.url}`;
+ break;
+ }
+ case ScopeType.Auditor: {
+ currencies[stringifyScopeInfoShort(b.scopeInfo)] =
+ `${b.scopeInfo.currency} ${b.scopeInfo.url}`;
+ break;
+ }
+ default: {
+ assertUnreachable(b.scopeInfo);
+ }
}
});
currencies[""] = "Select a currency";
@@ -103,55 +111,32 @@ export function useComponentState(props: Props): RecursiveState<State> {
status: "select-currency",
error: undefined,
onCurrencySelected: (c: string) => {
- setAmount(Amounts.zeroOfCurrency(c));
+ const scope = parseScopeInfoShort(c);
+ setScope(scope);
},
currencies,
};
};
}
- const currencyAndAmount = Amounts.stringify(amount);
- const invalid = Amounts.isZero(amount);
-
switch (props.type) {
case "send":
return {
status: "ready",
error: undefined,
previous,
- selectCurrency: {
- onClick: pushAlertOnError(async () => {
- setAmount(undefined);
- }),
- },
+ onSelectAccount: pushAlertOnError(async (account: PaytoUri) => {
+ props.goToWalletKnownBankDeposit(scope, account);
+ }),
goToBank: {
- onClick: invalid
- ? undefined
- : pushAlertOnError(async () => {
- props.goToWalletBankDeposit(currencyAndAmount);
- }),
- },
- selectMax: {
onClick: pushAlertOnError(async () => {
- const resp = await api.wallet.call(
- WalletApiOperation.GetMaxDepositAmount,
- {
- currency: amount.currency,
- },
- );
- setAmount(Amounts.parseOrThrow(resp.effectiveAmount));
+ props.goToWalletNewBankDeposit(scope);
}),
},
goToWallet: {
- onClick: invalid
- ? undefined
- : pushAlertOnError(async () => {
- props.goToWalletWalletSend(currencyAndAmount);
- }),
- },
- amountHandler: {
- onInput: pushAlertOnError(async (s) => setAmount(s)),
- value: amount,
+ onClick: pushAlertOnError(async () => {
+ props.goToWalletWalletSend(scope);
+ }),
},
type: props.type,
};
@@ -160,35 +145,16 @@ export function useComponentState(props: Props): RecursiveState<State> {
status: "ready",
error: undefined,
previous,
- selectCurrency: {
+ goToBank: {
onClick: pushAlertOnError(async () => {
- setAmount(undefined);
+ props.goToWalletManualWithdraw(scope);
}),
},
- selectMax: {
- onClick: invalid
- ? undefined
- : pushAlertOnError(async () => {
- props.goToWalletManualWithdraw(currencyAndAmount);
- }),
- },
- goToBank: {
- onClick: invalid
- ? undefined
- : pushAlertOnError(async () => {
- props.goToWalletManualWithdraw(currencyAndAmount);
- }),
- },
+ onSelectAccount: () => { },
goToWallet: {
- onClick: invalid
- ? undefined
- : pushAlertOnError(async () => {
- props.goToWalletWalletInvoice(currencyAndAmount);
- }),
- },
- amountHandler: {
- onInput: pushAlertOnError(async (s) => setAmount(s)),
- value: amount,
+ onClick: pushAlertOnError(async () => {
+ props.goToWalletWalletInvoice(scope);
+ }),
},
type: props.type,
};
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
index e1ac958f7..c530a7020 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
@@ -27,33 +27,15 @@ export default {
};
export const GetCash = tests.createExample(ReadyView, {
- amountHandler: {
- value: {
- currency: "EUR",
- fraction: 0,
- value: 2,
- },
- },
goToBank: {},
- selectMax: {},
goToWallet: {},
previous: [],
- selectCurrency: {},
type: "get",
});
export const SendCash = tests.createExample(ReadyView, {
- amountHandler: {
- value: {
- currency: "EUR",
- fraction: 0,
- value: 1,
- },
- },
- selectMax: {},
goToBank: {},
goToWallet: {},
previous: [],
- selectCurrency: {},
type: "send",
});
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
index 683378613..9e75f0b6f 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -25,7 +25,8 @@ import {
ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
- ScopeType,
+ ScopeInfo,
+ ScopeType
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as tests from "@gnu-taler/web-util/testing";
@@ -34,14 +35,15 @@ import { nullFunction } from "../../mui/handlers.js";
import { createWalletApiMock } from "../../test-utils.js";
import { useComponentState } from "./state.js";
+const currency = "ARS";
const exchangeArs: ExchangeListItem = {
- currency: "ARS",
- exchangeBaseUrl: "http://",
+ currency,
+ exchangeBaseUrl: "http://exchange.test.taler.net",
masterPub: "123qwe123",
scopeInfo: {
- currency: "ARS",
+ currency,
type: ScopeType.Exchange,
- url: "http://",
+ url: "http://exchange.test.taler.net",
},
tosStatus: ExchangeTosStatus.Accepted,
exchangeEntryStatus: ExchangeEntryStatus.Used,
@@ -54,19 +56,20 @@ const exchangeArs: ExchangeListItem = {
};
describe("Destination selection states", () => {
- it("should select currency if no amount specified", async () => {
+ it.skip("should select currency if no amount specified", async () => {
const { handler, TestingContext } = createWalletApiMock();
- handler.addWalletCallResponse(
- WalletApiOperation.ListExchanges,
- {},
- {
- exchanges: [exchangeArs],
- },
- );
+ handler.addWalletCallResponse(WalletApiOperation.ListExchanges, undefined, {
+ exchanges: [exchangeArs],
+ });
const props = {
type: "get" as const,
+ // scope: {
+ // currency: "ARS",
+ // type: ScopeType.Exchange,
+ // url: "http://asd.com",
+ // } as ScopeInfo,
goToWalletManualWithdraw: nullFunction,
goToWalletWalletInvoice: nullFunction,
};
@@ -82,7 +85,8 @@ describe("Destination selection states", () => {
if (state.status !== "select-currency") expect.fail();
if (state.error) expect.fail();
expect(state.currencies).deep.eq({
- ARS: "ARS",
+ "ARS/http%3A%2F%2Fexchange.test.taler.net":
+ "ARS http://exchange.test.taler.net",
"": "Select a currency",
});
@@ -94,9 +98,6 @@ describe("Destination selection states", () => {
expect(state.goToBank.onClick).eq(undefined);
expect(state.goToWallet.onClick).eq(undefined);
- expect(state.amountHandler.value).deep.eq(
- Amounts.parseOrThrow("ARS:0"),
- );
},
],
TestingContext,
@@ -106,14 +107,19 @@ describe("Destination selection states", () => {
expect(handler.getCallingQueueState()).eq("empty");
});
- it("should be possible to start with an amount specified in request params", async () => {
+ it.skip("should be possible to start with an amount specified in request params", async () => {
const { handler, TestingContext } = createWalletApiMock();
const props = {
type: "get" as const,
+ scope: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://asd.com",
+ } as ScopeInfo,
goToWalletManualWithdraw: nullFunction,
goToWalletWalletInvoice: nullFunction,
- amount: "ARS:2",
+ amount: Amounts.parseOrThrow("ARS:2"),
};
const hookBehavior = await tests.hookBehaveLikeThis(
@@ -129,19 +135,6 @@ describe("Destination selection states", () => {
expect(state.goToBank.onClick).not.eq(undefined);
expect(state.goToWallet.onClick).not.eq(undefined);
- expect(state.amountHandler.value).deep.eq(
- Amounts.parseOrThrow("ARS:2"),
- );
- },
- (state) => {
- if (state.status !== "ready") expect.fail();
- if (state.error) expect.fail();
- expect(state.goToBank.onClick).not.eq(undefined);
- expect(state.goToWallet.onClick).not.eq(undefined);
-
- expect(state.amountHandler.value).deep.eq(
- Amounts.parseOrThrow("ARS:2"),
- );
},
],
TestingContext,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index 8a74a20f1..cf34ceb35 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -14,11 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { KnownBankAccountsInfo, PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
-import { AmountField } from "../../components/AmountField.js";
-import { EnabledBySettings } from "../../components/EnabledBySettings.js";
import { SelectList } from "../../components/SelectList.js";
import {
Input,
@@ -33,7 +32,7 @@ import { Pages } from "../../NavigationBar.js";
import arrowIcon from "../../svg/chevron-down.inline.svg";
import bankIcon from "../../svg/ri-bank-line.inline.svg";
import { assertUnreachable } from "../../utils/index.js";
-import { Contact, State } from "./index.js";
+import { State } from "./index.js";
export function SelectCurrencyView({
currencies,
@@ -62,7 +61,7 @@ export function SelectCurrencyView({
</p>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div />
- <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}>
<i18n.Translate>Add an exchange</i18n.Translate>
</LinkPrimary>
</div>
@@ -81,7 +80,6 @@ const Container = styled.div`
const ContactTable = styled.table`
width: 100%;
& > tr > td {
- padding: 8px;
& > div:not([data-disabled]):hover {
background-color: lightblue;
}
@@ -192,10 +190,8 @@ export function ReadyView(props: State.Ready): VNode {
}
}
export function ReadyGetView({
- amountHandler,
goToBank,
goToWallet,
- selectCurrency,
previous,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -203,19 +199,8 @@ export function ReadyGetView({
return (
<Container>
<h1>
- <i18n.Translate>Specify the amount and the origin</i18n.Translate>
+ <i18n.Translate>Specify the origin</i18n.Translate>
</h1>
- <Grid container columns={2} justifyContent="space-between">
- <AmountField
- label={i18n.str`Amount`}
- required
- handler={amountHandler}
- />
-
- <Button onClick={selectCurrency.onClick}>
- <i18n.Translate>Change currency</i18n.Translate>
- </Button>
- </Grid>
<Grid container spacing={1} columns={1}>
{previous.length > 0 ? (
@@ -231,7 +216,7 @@ export function ReadyGetView({
<td>
<RowExample
info={info}
- disabled={!amountHandler.onInput}
+ // disabled={!amountHandler.onInput}
/>
</td>
</tr>
@@ -241,26 +226,11 @@ export function ReadyGetView({
</Grid>
</Fragment>
) : undefined}
- {previous.length > 0 ? (
- <Grid item>
- <p>
- <i18n.Translate>
- Or specify the origin of the money
- </i18n.Translate>
- </p>
- </Grid>
- ) : (
- <Grid item>
- <p>
- <i18n.Translate>Specify the origin of the money</i18n.Translate>
- </p>
- </Grid>
- )}
<Grid item container columns={2} spacing={1}>
<Grid item xs={1}>
<Paper style={{ padding: 8 }}>
<p>
- <i18n.Translate>From my bank account</i18n.Translate>
+ <i18n.Translate>From another bank account</i18n.Translate>
</p>
<Button onClick={goToBank.onClick}>
<i18n.Translate>Withdraw</i18n.Translate>
@@ -280,7 +250,7 @@ export function ReadyGetView({
<Grid item xs={1}>
<Paper style={{ padding: 8 }}>
<p>
- <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate>
+ <i18n.Translate>From a <pre style={{display:"inline"}}>taler://</pre> URI or QR code</i18n.Translate>
</p>
<a href={Pages.qr}>
<i18n.Translate>Enter URI here</i18n.Translate>
@@ -293,33 +263,19 @@ export function ReadyGetView({
);
}
export function ReadySendView({
- amountHandler,
goToBank,
+ onSelectAccount,
goToWallet,
previous,
- selectMax,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
return (
<Container>
<h1>
- <i18n.Translate>Specify the amount and the destination</i18n.Translate>
+ <i18n.Translate>Specify the destination</i18n.Translate>
</h1>
- <Grid container columns={2} justifyContent="space-between">
- <AmountField
- label={i18n.str`Amount`}
- required
- handler={amountHandler}
- />
- <EnabledBySettings name="advancedMode">
- <Button onClick={selectMax.onClick}>
- <i18n.Translate>Send all</i18n.Translate>
- </Button>
- </EnabledBySettings>
- </Grid>
-
<Grid container spacing={1} columns={1}>
{previous.length > 0 ? (
<Fragment>
@@ -334,7 +290,10 @@ export function ReadySendView({
<td>
<RowExample
info={info}
- disabled={!amountHandler.onInput}
+ onClick={() => {
+ onSelectAccount(info.uri)
+ }}
+ // disabled={!amountHandler.onInput}
/>
</td>
</tr>
@@ -344,28 +303,11 @@ export function ReadySendView({
</Grid>
</Fragment>
) : undefined}
- {previous.length > 0 ? (
- <Grid item>
- <p>
- <i18n.Translate>
- Or specify the destination of the money
- </i18n.Translate>
- </p>
- </Grid>
- ) : (
- <Grid item>
- <p>
- <i18n.Translate>
- Specify the destination of the money
- </i18n.Translate>
- </p>
- </Grid>
- )}
<Grid item container columns={2} spacing={1}>
<Grid item xs={1}>
<Paper style={{ padding: 8 }}>
<p>
- <i18n.Translate>To my bank account</i18n.Translate>
+ <i18n.Translate>To another bank account</i18n.Translate>
</p>
<Button onClick={goToBank.onClick}>
<i18n.Translate>Deposit</i18n.Translate>
@@ -391,31 +333,31 @@ export function ReadySendView({
function RowExample({
info,
disabled,
+ onClick
}: {
- info: Contact;
+ info: KnownBankAccountsInfo;
disabled?: boolean;
+ onClick?: () => void;
}): VNode {
- const icon = info.icon_type === "bank" ? bankIcon : undefined;
+
+
return (
- <MediaExample data-disabled={disabled}>
+ <MediaExample data-disabled={disabled} onClick={onClick}>
<MediaLeft>
<CircleDiv>
- {icon !== undefined ? (
<SvgIcon
- title={info.name}
+ title={info.alias}
dangerouslySetInnerHTML={{
- __html: icon,
+ __html: bankIcon,
}}
color="currentColor"
/>
- ) : (
- <span>A</span>
- )}
+
</CircleDiv>
</MediaLeft>
<MediaBody>
- <span>{info.name}</span>
- <LightText>{info.description}</LightText>
+ <span>{info.alias}</span>
+ <LightText>{describeAccount(info.uri)}</LightText>
</MediaBody>
<MediaRight>
<SvgIcon
@@ -428,3 +370,21 @@ function RowExample({
</MediaExample>
);
}
+
+
+function describeAccount(p: PaytoUri): string {
+ if (!p.isKnown) {
+ return stringifyPaytoUri(p)
+ }
+ switch (p.targetType) {
+ case "iban": {
+ return p.iban
+ }
+ case "x-taler-bank": {
+ return `${p.host}/${p.account}`
+ }
+ case "bitcoin": {
+ return `${p.address}`
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 8f23c0685..9c17d3cff 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -22,10 +22,13 @@ import {
LogLevel,
NotificationType,
ScopeType,
- stringifyWithdrawExchange
+ stringifyWithdrawExchange,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ encodeCrockForURI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -112,21 +115,21 @@ export function DeveloperPage(): VNode {
const currencies: { [ex: string]: string } = {};
const money_by_exchange = coins.reduce(
(prev, cur) => {
- const denom = Amounts.parseOrThrow(cur.denom_value);
- if (!prev[cur.exchange_base_url]) {
- prev[cur.exchange_base_url] = [];
- currencies[cur.exchange_base_url] = denom.currency;
+ const denom = Amounts.parseOrThrow(cur.denomValue);
+ if (!prev[cur.exchangeBaseUrl]) {
+ prev[cur.exchangeBaseUrl] = [];
+ currencies[cur.exchangeBaseUrl] = denom.currency;
}
- prev[cur.exchange_base_url].push({
+ prev[cur.exchangeBaseUrl].push({
// ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
denom_value: denom.value,
denom_fraction: denom.fraction,
// remain_value: parseFloat(
// Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
// ),
- status: cur.coin_status,
- from_refresh: cur.refresh_parent_coin_pub !== undefined,
- id: cur.coin_pub,
+ status: cur.coinStatus,
+ from_refresh: cur.refreshParentCoinPub !== undefined,
+ id: cur.coinPub,
});
return prev;
},
@@ -337,7 +340,13 @@ export function DeveloperPage(): VNode {
return (
<tr key={idx}>
<td>
- <a href={!uri ? undefined : Pages.defaultCta({ uri })}>
+ <a
+ href={
+ !uri
+ ? undefined
+ : `#${Pages.defaultCta({ uri: encodeCrockForURI(uri) })}`
+ }
+ >
{e.scopeInfo
? `${e.scopeInfo.currency} (${
e.scopeInfo.type === ScopeType.Global
@@ -351,7 +360,7 @@ export function DeveloperPage(): VNode {
<a
href={new URL(`/keys`, e.exchangeBaseUrl).href}
target="_blank"
- rel="noreferrer"
+ rel="noreferrer"
>
{e.exchangeBaseUrl}
</a>
@@ -461,7 +470,7 @@ export function DeveloperPage(): VNode {
)}
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div />
- <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}>
<i18n.Translate>Add an exchange</i18n.Translate>
</LinkPrimary>
</div>
@@ -667,6 +676,12 @@ function ShowAllCoins({
);
}
+/**
+ *
+ * @param str
+ * @deprecated FIXME: use a better base64 function
+ * @returns
+ */
function toBase64(str: string): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 482b8d698..470ad0514 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -70,6 +70,7 @@ const exampleData = {
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
confirmed: false,
exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
+ reserveClosingDelay: { d_us: "forever" },
type: WithdrawalType.ManualTransfer,
reserveIsReady: false,
},
@@ -168,7 +169,12 @@ export const SomeBalanceWithNoTransactions = tests.createExample(
transactionsByDate: {
"11/11/11": [],
},
- balances: [
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ balances: [
{
available: "TESTKUDOS:10" as AmountString,
flags: [],
@@ -183,7 +189,7 @@ export const SomeBalanceWithNoTransactions = tests.createExample(
},
},
],
- balanceIndex: 0,
+
},
);
@@ -191,6 +197,11 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, {
transactionsByDate: {
"11/11/11": [exampleData.withdraw],
},
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
balances: [
{
flags: [],
@@ -206,7 +217,7 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, {
},
},
],
- balanceIndex: 0,
+
});
export const TwoTransactionsAndZeroBalance = tests.createExample(
@@ -215,7 +226,12 @@ export const TwoTransactionsAndZeroBalance = tests.createExample(
transactionsByDate: {
"11/11/11": [exampleData.withdraw, exampleData.deposit],
},
- balances: [
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ balances: [
{
flags: [],
available: "USD:0" as AmountString,
@@ -230,7 +246,7 @@ export const TwoTransactionsAndZeroBalance = tests.createExample(
},
},
],
- balanceIndex: 0,
+
},
);
@@ -245,6 +261,11 @@ export const OneTransactionPending = tests.createExample(TestedComponent, {
},
],
},
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
balances: [
{
flags: [],
@@ -260,7 +281,7 @@ export const OneTransactionPending = tests.createExample(TestedComponent, {
},
},
],
- balanceIndex: 0,
+
});
export const SomeTransactions = tests.createExample(TestedComponent, {
@@ -282,6 +303,11 @@ export const SomeTransactions = tests.createExample(TestedComponent, {
exampleData.deposit,
],
},
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
balances: [
{
flags: [],
@@ -297,7 +323,7 @@ export const SomeTransactions = tests.createExample(TestedComponent, {
},
},
],
- balanceIndex: 0,
+
});
export const SomeTransactionsInDifferentStates = tests.createExample(
@@ -378,7 +404,12 @@ export const SomeTransactionsInDifferentStates = tests.createExample(
exampleData.deposit,
],
},
- balances: [
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ balances: [
{
flags: [],
available: "USD:10" as AmountString,
@@ -393,7 +424,7 @@ export const SomeTransactionsInDifferentStates = tests.createExample(
},
},
],
- balanceIndex: 0,
+
},
);
@@ -411,7 +442,12 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample(
exampleData.deposit,
],
},
- balances: [
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ balances: [
{
flags: [],
available: "USD:0" as AmountString,
@@ -439,7 +475,7 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample(
},
},
],
- balanceIndex: 0,
+
},
);
@@ -447,6 +483,11 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
transactionsByDate: {
"11/11/11": [exampleData.withdraw],
},
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
balances: [
{
flags: [],
@@ -514,7 +555,7 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
},
},
],
- balanceIndex: 0,
+
});
export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
@@ -523,7 +564,12 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
transactionsByDate: {
"11/11/11": [exampleData.withdraw],
},
- balances: [
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ balances: [
{
flags: [],
available: "USD:881001321230000" as AmountString,
@@ -590,7 +636,7 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
},
},
],
- balanceIndex: 0,
+
},
);
@@ -603,6 +649,11 @@ export const PeerToPeer = tests.createExample(TestedComponent, {
exampleData.push_debit,
],
},
+ scope: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
balances: [
{
flags: [],
@@ -618,5 +669,4 @@ export const PeerToPeer = tests.createExample(TestedComponent, {
},
},
],
- balanceIndex: 0,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index f81e6db9f..d67293920 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -18,9 +18,11 @@ import {
AbsoluteTime,
Amounts,
NotificationType,
+ ScopeInfo,
ScopeType,
Transaction,
WalletBalance,
+ stringifyScopeInfoShort,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -49,35 +51,34 @@ import { TextField } from "../mui/TextField.js";
import { TextFieldHandler } from "../mui/handlers.js";
interface Props {
- currency?: string;
+ scope?: ScopeInfo;
search?: boolean;
- goToWalletDeposit: (currency: string) => Promise<void>;
- goToWalletManualWithdraw: (currency?: string) => Promise<void>;
+ goToWalletDeposit: (scope: ScopeInfo) => Promise<void>;
+ goToWalletManualWithdraw: (scope?: ScopeInfo) => Promise<void>;
}
export function HistoryPage({
- currency: _c,
+ scope,
search: showSearch,
goToWalletManualWithdraw,
goToWalletDeposit,
}: Props): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
- const [balanceIndex, setBalanceIndex] = useState<number>(0);
+ const [selectedScope, setSelectedScope] = useState(scope);
const [search, setSearch] = useState<string>();
const [settings] = useSettings();
const state = useAsyncAsHook(async () => {
const b = await api.wallet.call(WalletApiOperation.GetBalances, {});
- const balance =
- b.balances.length > 0 ? b.balances[balanceIndex] : undefined;
+ const balances = b.balances;
const tx = await api.wallet.call(WalletApiOperation.GetTransactions, {
- scopeInfo: showSearch ? undefined : balance?.scopeInfo,
+ scopeInfo: showSearch ? undefined : selectedScope,
sort: "descending",
includeRefreshes: settings.showRefeshTransactions,
search,
});
- return { b, tx };
- }, [balanceIndex, search]);
+ return { balances, transactions: tx.transactions };
+ }, [selectedScope, search]);
useEffect(() => {
return api.listener.onUpdateNotification(
@@ -103,17 +104,17 @@ export function HistoryPage({
);
}
- if (!state.response.b.balances.length) {
+ if (!state.response.balances.length) {
return (
<NoBalanceHelp
goToWalletManualWithdraw={{
- onClick: pushAlertOnError(goToWalletManualWithdraw),
+ onClick: pushAlertOnError(() => goToWalletManualWithdraw(selectedScope)),
}}
/>
);
}
- const byDate = state.response.tx.transactions.reduce(
+ const txsByDate = state.response.transactions.reduce(
(rv, x) => {
const startDay =
x.timestamp.t_s === "never"
@@ -141,41 +142,48 @@ export function HistoryPage({
setSearch(d);
}),
}}
- transactionsByDate={byDate}
+ transactionsByDate={txsByDate}
/>
);
}
return (
<HistoryView
- balanceIndex={balanceIndex}
- changeBalanceIndex={(b) => setBalanceIndex(b)}
- balances={state.response.b.balances}
+ scope={selectedScope ?? state.response.balances[0].scopeInfo}
+ changeScope={(b) => setSelectedScope(b)}
+ balances={state.response.balances}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
- transactionsByDate={byDate}
+ transactionsByDate={txsByDate}
/>
);
}
export function HistoryView({
balances,
- balanceIndex,
- changeBalanceIndex,
+ scope,
+ changeScope,
transactionsByDate,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
- balanceIndex: number;
- changeBalanceIndex: (s: number) => void;
- goToWalletDeposit: (currency: string) => Promise<void>;
- goToWalletManualWithdraw: (currency?: string) => Promise<void>;
+ scope: ScopeInfo;
+ changeScope: (scope: ScopeInfo) => void;
+ goToWalletDeposit: (scope: ScopeInfo) => Promise<void>;
+ goToWalletManualWithdraw: (scope: ScopeInfo) => Promise<void>;
transactionsByDate: Record<string, Transaction[]>;
balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
+ const scopeStr = stringifyScopeInfoShort(scope);
+ const balanceIndex = balances.findIndex(
+ (b) => stringifyScopeInfoShort(b.scopeInfo) === scopeStr,
+ );
const balance = balances[balanceIndex];
+ if (!balance) {
+ return <div>unknown scope</div>;
+ }
const available = balance
? Amounts.jsonifyAmount(balance.available)
@@ -200,9 +208,7 @@ export function HistoryView({
tooltip="Transfer money to the wallet"
startIcon={DownloadIcon}
variant="contained"
- onClick={() =>
- goToWalletManualWithdraw(balance.scopeInfo.currency)
- }
+ onClick={() => goToWalletManualWithdraw(balance.scopeInfo)}
>
<i18n.Translate>Receive</i18n.Translate>
</Button>
@@ -212,7 +218,7 @@ export function HistoryView({
startIcon={UploadIcon}
variant="outlined"
color="primary"
- onClick={() => goToWalletDeposit(balance.scopeInfo.currency)}
+ onClick={() => goToWalletDeposit(balance.scopeInfo)}
>
<i18n.Translate>Send</i18n.Translate>
</Button>
@@ -238,9 +244,8 @@ export function HistoryView({
}}
value={balanceIndex}
onChange={(e) => {
- changeBalanceIndex(
- Number.parseInt(e.currentTarget.value, 10),
- );
+ const bIdx = Number.parseInt(e.currentTarget.value, 10);
+ changeScope(balances[bIdx].scopeInfo);
}}
>
{balances.map((entry, index) => {
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
index 3a00d48ce..a76d77709 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { KnownBankAccountsInfo } from "@gnu-taler/taler-util";
+import { KnownBankAccountsInfo, PaytoUri, ScopeInfo } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
@@ -28,8 +28,8 @@ import { useComponentState } from "./state.js";
import { ReadyView } from "./views.js";
export interface Props {
- currency: string;
- onAccountAdded: (uri: string) => void;
+ scope: ScopeInfo;
+ onAccountAdded: (uri: PaytoUri) => void;
onCancel: () => void;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
index a7b2fe90f..72727ec64 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
@@ -26,38 +26,31 @@ import { useBackendContext } from "../../context/backend.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { AccountByType, Props, State } from "./index.js";
-import { useSettings } from "../../hooks/useSettings.js";
export function useComponentState({
- currency,
+ scope,
onAccountAdded,
onCancel,
}: Props): State {
const api = useBackendContext();
const { pushAlertOnError } = useAlertContext();
const { i18n } = useTranslationContext();
+
const hook = useAsyncAsHook(() =>
- api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
+ api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency: scope.currency }),
);
- const accountType: Record<string, string> = {
- iban: "IBAN",
- };
- const [settings] = useSettings();
- if (settings.extendedAccountTypes) {
- accountType["bitcoin"] = "Bitcoin";
- accountType["x-taler-bank"] = "Taler Bank";
- }
- const [payto, setPayto] = useState("");
- const [alias, setAlias] = useState("");
- const [type, setType] = useState("iban");
+ const hook2 = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.GetDepositWireTypesForCurrency, { currency: scope.currency, scopeInfo: scope }),
+ );
- if (!hook) {
+ if (!hook || !hook2) {
return {
status: "loading",
error: undefined,
};
}
+
if (hook.hasError) {
return {
status: "error",
@@ -68,6 +61,44 @@ export function useComponentState({
};
}
+ if (hook2.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load supported wire methods`,
+ hook2),
+ };
+ }
+
+ if (hook2.response.wireTypes.length === 0) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`No wire methods supported for this currency`,
+ description: i18n.str``,
+ cause: new Error("something"),
+ context: {},
+ },
+ };
+ }
+
+ const [payto, setPayto] = useState("");
+ const [alias, setAlias] = useState("");
+ const [type, setType] = useState(hook2.response.wireTypes[0]);
+
+ const accountType: Record<string, string> = {};
+ hook2.response.wireTypes.forEach(t => {
+ if (t === "iban") {
+ accountType[t] = "IBAN"
+ } else if (t === "x-taler-bank") {
+ accountType[t] = "x-taler-bank"
+ } else if (t === "bitcoin") {
+ accountType[t] = "Bitcoin"
+ }
+ });
+
const uri = parsePaytoUri(payto);
const found =
hook.response.accounts.findIndex(
@@ -80,10 +111,10 @@ export function useComponentState({
const normalizedPayto = stringifyPaytoUri(uri);
await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, {
alias,
- currency,
+ currency: scope.currency,
payto: normalizedPayto,
});
- onAccountAdded(payto);
+ onAccountAdded(uri);
}
const paytoUriError = found ? "that account is already present" : undefined;
@@ -112,7 +143,7 @@ export function useComponentState({
return {
status: "ready",
error: undefined,
- currency,
+ currency:scope.currency,
accountType: {
list: accountType,
value: type,
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
index 03a08016a..d205b2fa9 100644
--- a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
@@ -104,7 +104,7 @@ function NotificationItem({
return (
<NotificationLayout
timestamp={timestamp}
- href={Pages.balanceTransaction({ tid: info.transactionId })}
+ href={`#${Pages.balanceTransaction({ tid: info.transactionId })}`}
title="Withdrawal on hold"
subtitle="Know-your-customer validation is required"
iconPath={"K"}
@@ -115,7 +115,7 @@ function NotificationItem({
return (
<NotificationLayout
timestamp={timestamp}
- href={Pages.balanceTransaction({ tid: info.transactionId })}
+ href={`#${Pages.balanceTransaction({ tid: info.transactionId })}`}
title="Merchant has refund your payment"
subtitle="Accept or deny refund"
iconPath={"K"}
@@ -126,7 +126,7 @@ function NotificationItem({
return (
<NotificationLayout
timestamp={timestamp}
- href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
+ href={`#${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
title="Backup provider is unpaid"
subtitle="Complete the payment or remove the service provider"
iconPath={"K"}
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index a01ea6967..9635cd077 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -216,7 +216,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
function onChangeDetect(str: string) {
if (str) {
- const uri = parseTalerUri(str);
+ const uri = parseTalerUri(str.toLowerCase());
if (!uri) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
@@ -233,7 +233,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
function onChange(str: string) {
if (str) {
- if (!parseTalerUri(str)) {
+ if (!parseTalerUri(str.toLowerCase())) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
);
@@ -293,7 +293,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
setError(i18n.str`something unexpected happen: ${error}`);
}
}
- const uri = parseTalerUri(value);
+ const uri = parseTalerUri(value.toLowerCase());
return (
<Container>
diff --git a/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx b/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx
new file mode 100644
index 000000000..e2388c961
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/SupportedBanksForAccount.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+
+interface Props {
+ account: PaytoUri;
+}
+
+export function SupportedBanksForAccount({ account }: Props): VNode {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const state = useAsyncAsHook(() => {
+ return api.wallet.call(WalletApiOperation.GetBankingChoicesForPayto, {
+ paytoUri: stringifyPaytoUri(account),
+ });
+ });
+ if (!state) {
+ return <Loading />;
+ }
+
+ if (state.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not bank choices for account`,
+ state,
+ )}
+ />
+ );
+ }
+
+ return (
+ <div>
+ {state.response.choices.map((ch) => {
+ return <a href={ch.uri}>{ch.label}</a>;
+ })}
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index 194f0e0bb..94d7e4c61 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -41,7 +41,7 @@ import {
TransactionType,
TransactionWithdrawal,
WithdrawalDetails,
- WithdrawalType
+ WithdrawalType,
} from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import beer from "../../static-dev/beer.png";
@@ -61,6 +61,7 @@ export default {
const commonTransaction: TransactionCommon = {
error: undefined,
amountRaw: "KUDOS:11" as AmountString,
+ scopes: [],
amountEffective: "KUDOS:9.2" as AmountString,
txState: {
major: TransactionMajorState.Done,
@@ -86,6 +87,7 @@ const exampleData = {
confirmed: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"],
+ reserveClosingDelay: { d_us: "forever" },
type: WithdrawalType.ManualTransfer,
},
} as TransactionWithdrawal,
@@ -281,12 +283,17 @@ export const WithdrawPendingManual = tests.createExample(
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: ["payto://iban/ES8877998399652238"],
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- exchangeCreditAccountDetails: [{
- paytoUri: "payto://IBAN/1231231231",
+ reserveClosingDelay: {
+ d_us: 111,
},
- {
- paytoUri: "payto://IBAN/2342342342",
- }],
+ exchangeCreditAccountDetails: [
+ {
+ paytoUri: "payto://IBAN/1231231231",
+ },
+ {
+ paytoUri: "payto://IBAN/2342342342",
+ },
+ ],
} as WithdrawalDetails,
txState: {
major: TransactionMajorState.Pending,
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 1f0293352..036f75c63 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -20,11 +20,14 @@ import {
Amounts,
AmountString,
DenomLossEventType,
+ Duration,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
+ ScopeInfo,
+ ScopeType,
stringifyPaytoUri,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -41,7 +44,10 @@ import {
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ encodeCrockForURI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
@@ -78,7 +84,7 @@ import { assertUnreachable } from "../utils/index.js";
interface Props {
tid: string;
- goToWalletHistory: (currency?: string) => Promise<void>;
+ goToWalletHistory: (scope: ScopeInfo) => Promise<void>;
}
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
@@ -116,7 +122,13 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
);
}
- const currency = Amounts.parse(state.response.amountRaw)?.currency;
+ const currency = Amounts.parse(state.response.amountEffective)!.currency;
+ const txScope = !state.response.scopes.length
+ ? {
+ type: ScopeType.Global as const,
+ currency,
+ }
+ : state.response.scopes[0];
return (
<TransactionView
@@ -125,44 +137,44 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
await api.wallet.call(WalletApiOperation.FailTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ // goToWalletHistory(txScope);
}}
onSuspend={async () => {
await api.wallet.call(WalletApiOperation.SuspendTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ // goToWalletHistory(txScope);
}}
onResume={async () => {
await api.wallet.call(WalletApiOperation.ResumeTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ // goToWalletHistory(txScope);
}}
onAbort={async () => {
await api.wallet.call(WalletApiOperation.AbortTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ // goToWalletHistory(txScope);
}}
onRetry={async () => {
await api.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ // goToWalletHistory(txScope);
}}
onDelete={async () => {
await api.wallet.call(WalletApiOperation.DeleteTransaction, {
transactionId,
});
- goToWalletHistory(currency);
+ goToWalletHistory(txScope);
}}
onRefund={async (transactionId) => {
await api.wallet.call(WalletApiOperation.StartRefundQuery, {
transactionId,
});
}}
- onBack={() => goToWalletHistory(currency)}
+ onBack={() => goToWalletHistory(txScope)}
/>
);
}
@@ -243,7 +255,9 @@ function TransactionTemplate({
/>
) : undefined}
{transaction.txState.major === TransactionMajorState.Pending &&
- (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ (transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor ===
+ TransactionMinorState.BalanceKycRequired ? (
<AlertView
alert={{
type: "warning",
@@ -268,14 +282,6 @@ function TransactionTemplate({
),
}}
/>
- ) : transaction.txState.minor ===
- TransactionMinorState.AmlRequired ? (
- <WarningBox>
- <i18n.Translate>
- The transaction has been blocked since the account required an
- AML check.
- </i18n.Translate>
- </WarningBox>
) : (
<WarningBox>
<div style={{ justifyContent: "center", lineHeight: "25px" }}>
@@ -452,8 +458,7 @@ export function TransactionView({
// ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
// : [];
const blockedByKycOrAml =
- transaction.txState.minor === TransactionMinorState.KycRequired ||
- transaction.txState.minor === TransactionMinorState.AmlRequired;
+ transaction.txState.minor === TransactionMinorState.KycRequired;
return (
<TransactionTemplate
transaction={transaction}
@@ -520,6 +525,30 @@ export function TransactionView({
/>
}
/>
+ {transaction.txState.major === TransactionMajorState.Aborted &&
+ transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer ? (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Withdrawal incomplete.`,
+ description: (
+ <i18n.Translate>
+ If you have already sent money to the service provider account
+ it will wire it back at{" "}
+ <Time
+ timestamp={AbsoluteTime.addDuration(
+ AbsoluteTime.fromPreciseTimestamp(transaction.timestamp),
+ Duration.fromTalerProtocolDuration(
+ transaction.withdrawalDetails.reserveClosingDelay,
+ ),
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </i18n.Translate>
+ ),
+ }}
+ />
+ ) : undefined}
</TransactionTemplate>
);
}
@@ -575,9 +604,9 @@ export function TransactionView({
<i18n.Translate>
{<Amount value={r.amountEffective} />}{" "}
<a
- href={Pages.balanceTransaction({
+ href={`#${Pages.balanceTransaction({
tid: r.transactionId,
- })}
+ })}`}
>
was refunded
</a>{" "}
@@ -714,9 +743,19 @@ export function TransactionView({
) : transaction.wireTransferProgress === 0 ? (
<AlertView
alert={{
- type: "warning",
- message: i18n.str`Wire transfer is not initiated.`,
- description: i18n.str` `,
+ type: "info",
+ message: i18n.str`Wire transfer still pending.`,
+ description: (
+ <i18n.Translate>
+ The service provider deadline to make the wire transfer is:{" "}
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ transaction.wireTransferDeadline,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </i18n.Translate>
+ ),
}}
/>
) : transaction.wireTransferProgress === 100 ? (
@@ -743,7 +782,17 @@ export function TransactionView({
alert={{
type: "info",
message: i18n.str`Wire transfer in progress.`,
- description: i18n.str` `,
+ description: (
+ <i18n.Translate>
+ The service provider deadline to make the wire transfer is:{" "}
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ transaction.wireTransferDeadline,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </i18n.Translate>
+ ),
}}
/>
)}
@@ -801,9 +850,9 @@ export function TransactionView({
>
{transaction.paymentInfo ? (
<a
- href={Pages.balanceTransaction({
+ href={`#${Pages.balanceTransaction({
tid: transaction.refundedTransactionId,
- })}
+ })}`}
>
{transaction.paymentInfo.summary}
</a>
@@ -1416,9 +1465,11 @@ export function TransferPickupDetails({
export function WithdrawDetails({
conversion,
amount,
+ bankFee,
}: {
conversion?: AmountJson;
amount: AmountWithFee;
+ bankFee?: AmountJson;
}): VNode {
const { i18n } = useTranslationContext();
@@ -1481,6 +1532,16 @@ export function WithdrawDetails({
</tr>
</Fragment>
)}
+ {!bankFee || Amounts.isZero(bankFee) ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Bank fee</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={bankFee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
</PurchaseDetailsTable>
);
}
@@ -1734,7 +1795,7 @@ function TrackingDepositDetails({
);
}
-function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
+export function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
return (
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 47b466fcd..8361a098d 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -31,7 +31,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
- WalletNotification
+ WalletNotification,
} from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
@@ -55,7 +55,7 @@ import { WalletActivityTrack } from "./wxBackend.js";
const logger = new Logger("wxApi");
-export const WALLET_CORE_SUPPORTED_VERSION = "5:0:0"
+export const WALLET_CORE_SUPPORTED_VERSION = "7:0:0";
export interface ExtendedPermissionsResponse {
newValue: boolean;
@@ -77,6 +77,7 @@ export interface BackgroundOperations {
getNotifications: {
request: {
filter: string;
+ operationsFrom?: string;
};
response: WalletActivityTrack[];
};
@@ -93,7 +94,10 @@ export interface BackgroundOperations {
};
}
-export type WalletEvent = { notification: WalletNotification, when: AbsoluteTime }
+export type WalletEvent = {
+ notification: WalletNotification;
+ when: AbsoluteTime;
+};
export interface BackgroundApiClient {
call<Op extends keyof BackgroundOperations>(
@@ -139,10 +143,14 @@ class BackgroundApiClientImpl implements BackgroundApiClient {
response = await platform.sendMessageToBackground(message);
} catch (error) {
if (error instanceof Error) {
- throw new BackgroundError(operation, {
- code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
- when: AbsoluteTime.now(),
- }, error);
+ throw new BackgroundError(
+ operation,
+ {
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ when: AbsoluteTime.now(),
+ },
+ error,
+ );
}
throw error;
}
@@ -168,6 +176,8 @@ class WalletApiClientImpl implements WalletCoreApiClient {
): Promise<WalletCoreResponseType<Op>> {
let response: CoreApiResponse;
try {
+ // FIXME: This type must be fixed and needs documentation!
+ // @ts-ignore
const message: MessageFromFrontendWallet<Op> = {
channel: "wallet",
operation,
@@ -176,10 +186,14 @@ class WalletApiClientImpl implements WalletCoreApiClient {
response = await platform.sendMessageToBackground(message);
} catch (error) {
if (error instanceof Error) {
- throw new BackgroundError(operation, {
- code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
- when: AbsoluteTime.now(),
- }, error);
+ throw new BackgroundError(
+ operation,
+ {
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ when: AbsoluteTime.now(),
+ },
+ error,
+ );
}
throw error;
}
@@ -187,7 +201,7 @@ class WalletApiClientImpl implements WalletCoreApiClient {
throw new BackgroundError(
`Wallet operation "${operation}" failed`,
response.error,
- TalerError.fromUncheckedDetail(response.error)
+ TalerError.fromUncheckedDetail(response.error),
);
}
logger.trace("got response", response);
@@ -205,7 +219,9 @@ function onUpdateNotification(
return;
};
const onNewMessage = (message: MessageFromBackend): void => {
- const shouldNotify = message.type === "wallet" && messageTypes.includes(message.notification.type);
+ const shouldNotify =
+ message.type === "wallet" &&
+ messageTypes.includes(message.notification.type);
if (shouldNotify) {
doCallback(message.notification);
}
@@ -226,7 +242,7 @@ function trigger(w: ExtensionNotification) {
platform.triggerWalletEvent({
type: "web-extension",
notification: w,
- })
+ });
}
export const wxApi = {
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index a0b9f2908..53028f906 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -91,19 +91,15 @@ async function resetDb(): Promise<void> {
}
export type WalletActivityTrack = {
- id: number;
+ // id: number;
events: (WalletNotification & { when: AbsoluteTime })[];
start: AbsoluteTime;
type: NotificationType;
end: AbsoluteTime;
groupId: string;
+ requestId?: string; //only for request event
};
-let counter = 0;
-function getUniqueId(): number {
- return counter++;
-}
-
//FIXME: maybe circular buffer
const activity: WalletActivityTrack[] = [];
@@ -120,10 +116,9 @@ function convertWalletActivityNotification(
if (found) {
found.end = event.when;
found.events.unshift(event);
- return found;
+ return undefined;
}
return {
- id: getUniqueId(),
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
@@ -134,7 +129,6 @@ function convertWalletActivityNotification(
case NotificationType.BackupOperationError: {
const groupId = "";
return {
- id: getUniqueId(),
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
@@ -148,10 +142,9 @@ function convertWalletActivityNotification(
if (found) {
found.end = event.when;
found.events.unshift(event);
- return found;
+ return undefined;
}
return {
- id: getUniqueId(),
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
@@ -168,10 +161,9 @@ function convertWalletActivityNotification(
if (found) {
found.end = event.when;
found.events.unshift(event);
- return found;
+ return undefined;
}
return {
- id: getUniqueId(),
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
@@ -181,14 +173,13 @@ function convertWalletActivityNotification(
}
case NotificationType.Idle: {
const groupId = "";
- return({
- id: getUniqueId(),
+ return {
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
events: [event],
groupId,
- });
+ };
}
case NotificationType.TaskObservabilityEvent: {
const groupId = `${event.type}:${event.taskId}`;
@@ -196,16 +187,15 @@ function convertWalletActivityNotification(
if (found) {
found.end = event.when;
found.events.unshift(event);
- return found;
+ return undefined;
}
- return({
- id: getUniqueId(),
+ return {
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
events: [event],
groupId,
- });
+ };
}
case NotificationType.RequestObservabilityEvent: {
const groupId = `${event.type}:${event.operation}:${event.requestId}`;
@@ -213,16 +203,16 @@ function convertWalletActivityNotification(
if (found) {
found.end = event.when;
found.events.unshift(event);
- return found;
+ return undefined;
}
- return({
- id: getUniqueId(),
+ return {
type: event.type,
start: event.when,
end: AbsoluteTime.never(),
events: [event],
groupId,
- });
+ requestId: event.requestId,
+ };
}
}
}
@@ -241,14 +231,24 @@ function addNewWalletActivityNotification(
async function getNotifications({
filter,
+ operationFrom,
}: {
filter: string;
+ operationFrom?: string;
}): Promise<WalletActivityTrack[]> {
- if (!filter) return activity;
-
const rg = new RegExp(`.*${filter}.*`);
return activity.filter((event) => {
- return rg.test(event.groupId.toLowerCase());
+ if (operationFrom) {
+ if (event.type !== NotificationType.RequestObservabilityEvent) {
+ return false;
+ }
+ if (event.requestId) {
+ return event.requestId.startsWith(operationFrom);
+ }
+ }
+ if (!filter) return true;
+ const testFilter = rg.test(event.groupId.toLowerCase());
+ return testFilter;
});
}
@@ -318,7 +318,10 @@ let nextMessageIndex = 0;
async function dispatch<
Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
->(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+>(
+ req: MessageFromFrontend<Op> & { id: string },
+ from: string,
+): Promise<MessageResponse> {
nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
switch (req.channel) {
@@ -402,7 +405,7 @@ async function dispatch<
};
}
//multiple client can create the same id, send the wallet an unique key
- const newId = `${req.id}_${nextMessageIndex}`;
+ const newId = `${from}:${req.id}`;
const resp = await w.handleCoreApiRequest(
req.operation,
newId,
@@ -480,6 +483,7 @@ async function reinitWallet(): Promise<void> {
return;
}
wallet.addNotificationListener((message) => {
+ logger.trace("Wallet -> Webex", message);
if (settings.showWalletActivity) {
addNewWalletActivityNotification(activity, message);
}
@@ -516,10 +520,11 @@ export async function wxMain(): Promise<void> {
// Handlers for messages coming directly from the content
// script on the page
logger.trace("listen all channels");
- platform.listenToAllChannels(async (message) => {
+ platform.listenToAllChannels(async (message, from) => {
//wait until wallet is initialized
+ logger.trace("Webex -> Wallet", message);
await afterWalletIsInitialized;
- const result = await dispatch(message);
+ const result = await dispatch(message, from);
return result;
});
@@ -570,8 +575,6 @@ async function processWalletNotification(message: WalletNotification) {
message.type === NotificationType.TransactionStateTransition &&
(message.newTxState.minor === TransactionMinorState.KycRequired ||
message.oldTxState.minor === TransactionMinorState.KycRequired ||
- message.newTxState.minor === TransactionMinorState.AmlRequired ||
- message.oldTxState.minor === TransactionMinorState.AmlRequired ||
message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
) {
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index c6bf20160..12d5b8c09 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.11.4",
+ "version": "0.13.4",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
@@ -28,7 +28,6 @@
},
"scripts": {
"compile": "tsc && ./build.mjs",
- "build": "tsc && ./build.mjs",
"clean": "rm -rf dist lib tsconfig.tsbuildinfo",
"pretty": "prettier --write src"
},
diff --git a/packages/bank-ui/src/components/Time.tsx b/packages/web-util/src/components/Time.tsx
index 5c8afe212..9057b34f0 100644
--- a/packages/bank-ui/src/components/Time.tsx
+++ b/packages/web-util/src/components/Time.tsx
@@ -15,7 +15,6 @@
*/
import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
import {
formatISO,
format,
@@ -23,6 +22,7 @@ import {
intervalToDuration,
} from "date-fns";
import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "../index.browser.js";
/**
*
diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts
index d7ea41874..63231f8a2 100644
--- a/packages/web-util/src/components/index.ts
+++ b/packages/web-util/src/components/index.ts
@@ -10,3 +10,4 @@ export * from "./Button.js";
export * from "./ShowInputErrorLabel.js";
export * from "./NotificationBanner.js";
export * from "./ToastBanner.js";
+export * from "./Time.js";
diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts
index c1eaa37f8..89561e239 100644
--- a/packages/web-util/src/context/api.ts
+++ b/packages/web-util/src/context/api.ts
@@ -19,7 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+import {
+ TalerBankIntegrationHttpClient,
+ TalerCoreBankHttpClient,
+ TalerRevenueHttpClient,
+ TalerWireGatewayHttpClient,
+} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { defaultRequestHandler } from "../utils/request.js";
@@ -29,10 +34,10 @@ interface Type {
* @deprecated this show not be used
*/
request: typeof defaultRequestHandler;
- bankCore: TalerCoreBankHttpClient,
- bankIntegration: TalerBankIntegrationHttpClient,
- bankWire: TalerWireGatewayHttpClient,
- bankRevenue: TalerRevenueHttpClient,
+ bankCore: TalerCoreBankHttpClient;
+ bankIntegration: TalerBankIntegrationHttpClient;
+ bankWire: TalerWireGatewayHttpClient;
+ bankRevenue: TalerRevenueHttpClient;
}
const Context = createContext<Type>({ request: defaultRequestHandler } as any);
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
index 3f6a32f4b..e610b49e0 100644
--- a/packages/web-util/src/context/bank-api.ts
+++ b/packages/web-util/src/context/bank-api.ts
@@ -16,6 +16,7 @@
import {
CacheEvictor,
+ TalerCorebankConfigResponse,
LibtoolVersion,
ObservabilityEvent,
ObservableHttpClientLibrary,
@@ -24,7 +25,6 @@ import {
TalerBankConversionHttpClient,
TalerCoreBankCacheEviction,
TalerCoreBankHttpClient,
- TalerCorebankApi,
TalerError,
} from "@gnu-taler/taler-util";
import {
@@ -35,9 +35,9 @@ import {
h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js";
import { useTranslationContext } from "./translation.js";
-import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
/**
*
@@ -46,7 +46,7 @@ import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
export type BankContextType = {
url: URL;
- config: TalerCorebankApi.Config;
+ config: TalerCorebankConfigResponse;
lib: BankLib;
hints: VersionHint[];
onActivity: Subscriber<ObservabilityEvent>;
@@ -88,7 +88,7 @@ export const BankApiProvider = ({
frameOnError: FunctionComponent<{ children: ComponentChildren }>;
}): VNode => {
const [checked, setChecked] =
- useState<ConfigResult<TalerCorebankApi.Config>>();
+ useState<ConfigResult<TalerCorebankConfigResponse>>();
const { i18n } = useTranslationContext();
const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
@@ -165,7 +165,7 @@ export const BankApiProvider = ({
function buildBankApiClient(
url: URL,
evictors: Evictors,
-): APIClient<BankLib, TalerCorebankApi.Config> {
+): APIClient<BankLib, TalerCorebankConfigResponse> {
const httpFetch = new BrowserFetchHttpLib({
enableThrottling: true,
requireTls: false,
@@ -189,10 +189,14 @@ function buildBankApiClient(
httpLib,
);
- async function getRemoteConfig(): Promise<TalerCorebankApi.Config> {
+ async function getRemoteConfig(): Promise<TalerCorebankConfigResponse> {
const resp = await bank.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get bank remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts
index 8748f5f69..e2a6e05c3 100644
--- a/packages/web-util/src/context/challenger-api.ts
+++ b/packages/web-util/src/context/challenger-api.ts
@@ -183,7 +183,11 @@ function buildChallengerApiClient(
async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> {
const resp = await challenger.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get challenger remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts
index 39f889ba9..967b042f9 100644
--- a/packages/web-util/src/context/exchange-api.ts
+++ b/packages/web-util/src/context/exchange-api.ts
@@ -187,7 +187,11 @@ function buildExchangeApiClient(
async function getRemoteConfig(): Promise<TalerExchangeApi.ExchangeVersionResponse> {
const resp = await ex.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get exchange remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts
index 03c95d48e..8d929ae12 100644
--- a/packages/web-util/src/context/merchant-api.ts
+++ b/packages/web-util/src/context/merchant-api.ts
@@ -49,7 +49,7 @@ import {
export type MerchantContextType = {
url: URL;
- config: TalerMerchantApi.VersionResponse;
+ config: TalerMerchantApi.TalerMerchantConfigResponse;
lib: MerchantLib;
hints: VersionHint[];
onActivity: Subscriber<ObservabilityEvent>;
@@ -95,11 +95,13 @@ export const MerchantApiProvider = ({
evictors?: Evictors;
children: ComponentChildren;
frameOnError: FunctionComponent<{
- state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+ state:
+ | ConfigResultFail<TalerMerchantApi.TalerMerchantConfigResponse>
+ | undefined;
}>;
}): VNode => {
const [checked, setChecked] =
- useState<ConfigResult<TalerMerchantApi.VersionResponse>>();
+ useState<ConfigResult<TalerMerchantApi.TalerMerchantConfigResponse>>();
const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl);
@@ -162,7 +164,7 @@ export const MerchantApiProvider = ({
function buildMerchantApiClient(
url: URL,
evictors: Evictors,
-): APIClient<MerchantLib, TalerMerchantApi.VersionResponse> {
+): APIClient<MerchantLib, TalerMerchantApi.TalerMerchantConfigResponse> {
const httpFetch = new BrowserFetchHttpLib({
enableThrottling: true,
requireTls: false,
@@ -193,10 +195,14 @@ function buildMerchantApiClient(
return api.lib;
}
- async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> {
+ async function getRemoteConfig(): Promise<TalerMerchantApi.TalerMerchantConfigResponse> {
const resp = await instance.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get merchant remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts
index c2f2bbbc1..bd756318b 100644
--- a/packages/web-util/src/context/navigation.ts
+++ b/packages/web-util/src/context/navigation.ts
@@ -22,6 +22,7 @@ import {
Location,
findMatch,
RouteDefinition,
+ LocationNotFound,
} from "../utils/route.js";
/**
@@ -44,7 +45,7 @@ export const useNavigationContext = (): Type => useContext(Context);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
pagesMap: T,
-): Location<T> | undefined {
+): Location<T> | LocationNotFound<T> {
const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
const { path, params } = useNavigationContext();
diff --git a/packages/web-util/src/context/translation.ts b/packages/web-util/src/context/translation.ts
index 2725dc7e1..3e3ad2d13 100644
--- a/packages/web-util/src/context/translation.ts
+++ b/packages/web-util/src/context/translation.ts
@@ -95,7 +95,7 @@ export const TranslationProvider = ({
if (forceLang) {
changeLanguage(forceLang);
}
- });
+ },[forceLang]);
useEffect(() => {
setupI18n(lang, source);
}, [lang]);
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
index 338460170..239577e24 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -1,12 +1,21 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
+import {
+ UIFormElementConfig,
+ getConverterById,
+ useTranslationContext,
+} from "../index.browser.js";
import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
-import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ RenderAllFieldsByUiConfig,
+ UIFormField,
+ convertUiField,
+} from "./forms.js";
// import { FlexibleForm } from "./ui-form.js";
/**
* Flexible form uses a DoubleColumForm for design
- * and may have a dynamic properties defined by
+ * and may have a dynamic properties defined by
* behavior function.
*/
export interface FlexibleForm_Deprecated<T extends object> {
@@ -16,17 +25,19 @@ export interface FlexibleForm_Deprecated<T extends object> {
/**
* Double column form
- *
+ *
* Form with sections, every sections have a title and may
* have a description.
* Every sections contain a set of fields.
*/
-export type DoubleColumnForm_Deprecated = Array<DoubleColumnFormSection_Deprecated | undefined>;
+export type DoubleColumnForm_Deprecated = Array<
+ DoubleColumnFormSection_Deprecated | undefined
+>;
export type DoubleColumnFormSection_Deprecated = {
title: TranslatedString;
description?: TranslatedString;
- fields: UIFormField[];
+ fields: UIFormElementConfig[];
};
/**
@@ -40,20 +51,25 @@ export function DefaultForm<T extends object>({
onSubmit,
children,
readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm_Deprecated<T> }): VNode {
+}: Omit<FormProviderProps<T>, "computeFormState"> & {
+ form: FlexibleForm_Deprecated<T>;
+}): VNode {
+ const { i18n } = useTranslationContext();
return (
<FormProvider
initial={initial}
onUpdate={onUpdate}
onSubmit={onSubmit}
readOnly={readOnly}
- // computeFormState={form.behavior}
>
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
{form.design.map((section, i) => {
if (!section) return <Fragment />;
return (
- <div key={i} class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
{section.title}
@@ -69,7 +85,12 @@ export function DefaultForm<T extends object>({
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
key={i}
- fields={section.fields}
+ fields={convertUiField(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
/>
</div>
</div>
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
index 5e08efb32..fe886030a 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -14,7 +14,7 @@ export interface FormType<T extends object> {
computeFormState?: (v: Partial<T>) => FormState<T>;
}
-export const FormContext = createContext<FormType<any>| undefined>(undefined);
+export const FormContext = createContext<FormType<any> | undefined>(undefined);
/**
* Map of {[field]:FieldUIOptions}
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 6b792bfee..2f67f952f 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Absolute Time",
@@ -38,23 +39,28 @@ export namespace Simplest {
type TargetObject = {
today: AbsoluteTime;
-}
+};
const initial: TargetObject = {
- today: AbsoluteTime.now()
-}
+ today: AbsoluteTime.now(),
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "absoluteTimeText",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "today",
- pattern: "dd/MM/yyyy HH:mm"
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "absoluteTimeText",
+ label: "label of the field" as TranslatedString,
+ id: "today" as UIHandlerId,
+ pattern: "dd/MM/yyyy HH:mm",
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx
index f05887515..425824b75 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Amount",
@@ -38,22 +39,28 @@ export namespace Simplest {
type TargetObject = {
amount: AmountJson;
-}
+};
const initial: TargetObject = {
- amount: Amounts.parseOrThrow("USD:10")
-}
+ amount: Amounts.parseOrThrow("USD:10"),
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "amount",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "amount",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "amount",
+ label: "label of the field" as TranslatedString,
+ id: "amount" as UIHandlerId,
+ currency: "ARS",
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx
index 143e73f02..558e2e2aa 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Array",
@@ -41,39 +42,34 @@ type TargetObject = {
name: string;
age: number;
}[];
-}
+};
const initial: TargetObject = {
- people: [{
- name: "me",
- age: 17,
- }]
-}
+ people: [
+ {
+ name: "me",
+ age: 17,
+ },
+ ],
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "array",
- properties: {
- label: "People" as TranslatedString,
- name: "comment",
- fields: [{
- type: "text",
- properties: {
- label: "the name" as TranslatedString,
- name: "name",
- }
- }, {
- type: "integer",
- properties: {
- label: "the age" as TranslatedString,
- name: "age",
- }
- }],
- labelField: "name"
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "array",
+ label: "People" as TranslatedString,
+ fields: [],
+ id: "name" as UIHandlerId,
+ labelFieldId: "ame" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
index d90028508..58f403035 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -99,7 +99,7 @@ export function InputArray<T extends object, K extends keyof T>(
const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
const selected =
selectedIndex === undefined ? undefined : list[selectedIndex];
-
+
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -110,7 +110,7 @@ export function InputArray<T extends object, K extends keyof T>(
<div class="-space-y-px rounded-md bg-white ">
{list.map((v, idx) => {
- const label = getValueDeeper(v, labelField.split("."))
+ const label = getValueDeeper(v, labelField.split("."));
return (
<Option
label={label as TranslatedString}
@@ -204,8 +204,6 @@ export function InputArray<T extends object, K extends keyof T>(
);
}
-
-
export function getValueDeeper(
object: Record<string, any>,
names: string[],
@@ -218,9 +216,7 @@ export function getValueDeeper(
return getValueDeeper(object, rest);
}
if (object === undefined) {
- return ""
+ return "";
}
return getValueDeeper(object[head], rest);
}
-
-
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index 786dfe5bc..fbff14d36 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Choice Horizontal",
@@ -38,32 +39,41 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "0"
-}
+ comment: "0",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "choiceHorizontal",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- choices: [{
- label: "first choice" as TranslatedString,
- value: "1"
- }, {
- label: "second choice" as TranslatedString,
- value: "2"
- }, {
- label: "third choice" as TranslatedString,
- value: "3"
- },],
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ choices: [
+ {
+ label: "first choice" as TranslatedString,
+ value: "1",
+ },
+ {
+ label: "second choice" as TranslatedString,
+ value: "2",
+ },
+ {
+ label: "third choice" as TranslatedString,
+ value: "3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index 9a634d05c..5ee893b96 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Choice Stacked",
@@ -38,32 +39,41 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "choiceStacked",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- choices: [{
- label: "first choice" as TranslatedString,
- value: "1"
- }, {
- label: "second choice" as TranslatedString,
- value: "2"
- }, {
- label: "third choice" as TranslatedString,
- value: "3"
- },],
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ choices: [
+ {
+ label: "first choice" as TranslatedString,
+ value: "1",
+ },
+ {
+ label: "second choice" as TranslatedString,
+ value: "2",
+ },
+ {
+ label: "third choice" as TranslatedString,
+ value: "3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx
index eff18d071..58fb16835 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input File",
@@ -38,27 +39,32 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "file",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- required: true,
- maxBites: 2 * 1024 * 1024,
- accept: ".png",
- tooltip: "this is a very long tooltip that explain what the field does without being short" as TranslatedString,
- help: "Max size of 2 mega bytes" as TranslatedString,
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "file",
+ label: "label of the field" as TranslatedString,
+ required: true,
+ id: "comment" as UIHandlerId,
+ accept: ".png",
+ tooltip:
+ "this is a very long tooltip that explain what the field does without being short" as TranslatedString,
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx
index 378736a24..fd7fc39ed 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -25,31 +25,36 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Integer",
};
-
type TargetObject = {
age: number;
-}
+};
const initial: TargetObject = {
age: 5,
-}
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "integer",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "age",
- tooltip: "just numbers" as TranslatedString,
- },
- }]
- }]
-}
-
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "integer",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ tooltip: "just numbers" as TranslatedString,
+ },
+ ],
+ },
+ ],
+};
+
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx
index dea5c142a..32b26329b 100644
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Line",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
index eb3238ef9..4c0176195 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -59,16 +59,22 @@ export function LabelWithTooltipMaybeRequired({
);
if (required) {
return (
- <div class="flex justify-between">
+ <div class="flex justify-between w-fit">
{WithTooltip}
- <span class="text-sm leading-6 text-red-600">*</span>
+ <span class="text-sm leading-6 text-red-600 pl-2">*</span>
</div>
);
}
return WithTooltip;
}
-export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode {
+export function RenderAddon({
+ disabled,
+ addon,
+}: {
+ disabled?: boolean;
+ addon: Addon;
+}): VNode {
switch (addon.type) {
case "text": {
return (
@@ -115,7 +121,7 @@ function InputWrapper<T extends object, K extends keyof T>({
children: ComponentChildren;
} & UIFormProps<T, K>): VNode {
return (
- <div class="sm:col-span-6">
+ <div class="sm:col-span-6 ">
<LabelWithTooltipMaybeRequired
label={label}
required={required}
@@ -154,7 +160,7 @@ type InputType = "text" | "text-area" | "password" | "email" | "number";
export function InputLine<T extends object, K extends keyof T>(
props: { type: InputType } & UIFormProps<T, K>,
): VNode {
- const { name, placeholder, before, after, converter, type } = props;
+ const { name, placeholder, before, after, converter, type, disabled } = props;
//FIXME: remove deprecated
const fieldCtx = useField<T, K>(props.name);
const { value, onChange, state, error } =
@@ -222,7 +228,7 @@ export function InputLine<T extends object, K extends keyof T>(
<InputWrapper<T, K>
{...props}
help={props.help ?? state.help}
- disabled={state.disabled ?? false}
+ disabled={disabled ?? false}
error={showError ? error : undefined}
>
<textarea
@@ -234,7 +240,7 @@ export function InputLine<T extends object, K extends keyof T>(
placeholder={placeholder ? placeholder : undefined}
value={toString(value) ?? ""}
// defaultValue={toString(value)}
- disabled={state.disabled}
+ disabled={disabled ?? false}
aria-invalid={showError}
// aria-describedby="email-error"
class={clazz}
@@ -247,7 +253,7 @@ export function InputLine<T extends object, K extends keyof T>(
<InputWrapper<T, K>
{...props}
help={props.help ?? state.help}
- disabled={state.disabled ?? false}
+ disabled={disabled ?? false}
error={showError ? error : undefined}
>
<input
@@ -262,7 +268,7 @@ export function InputLine<T extends object, K extends keyof T>(
// onChange(fromString(value as any));
// }}
// defaultValue={toString(value)}
- disabled={state.disabled}
+ disabled={disabled ?? false}
aria-invalid={showError}
// aria-describedby="email-error"
class={clazz}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index ab17545f5..210ab1b2b 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Select Multiple",
@@ -39,52 +40,64 @@ export namespace Simplest {
type TargetObject = {
pets: string[];
things: string[];
-}
+};
const initial: TargetObject = {
pets: [],
things: [],
-}
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "selectMultiple",
- properties: {
- label: "allow diplicates" as TranslatedString,
- name: "pets",
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }, {
- type: "selectMultiple",
- properties: {
- label: "unique values" as TranslatedString,
- name: "things",
- unique: true,
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "selectMultiple",
+ label: "allow diplicates" as TranslatedString,
+ id: "pets" as UIHandlerId,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ {
+ type: "selectMultiple",
+ label: "unique values" as TranslatedString,
+ id: "things" as UIHandlerId,
+ unique: true,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index 2ebde3096..56284f4ab 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Select One",
@@ -38,33 +39,42 @@ export namespace Simplest {
type TargetObject = {
things: string;
-}
+};
const initial: TargetObject = {
- things: "one"
-}
+ things: "one",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "selectOne",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "things",
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "selectOne",
+ label: "label of the field" as TranslatedString,
+ id: "things" as UIHandlerId,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx
index 60b6ca224..3fa171655 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Text",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx
index ab1a695f5..c3df4f186 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -25,6 +25,7 @@ import {
DefaultForm as TestedComponent,
FlexibleForm_Deprecated,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Text Area",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx
index fcc57ffe2..7ce3cec5a 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -25,6 +25,7 @@ import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Toggle",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "toggle",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "toggle",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
index 4c5050830..2c789b9a3 100644
--- a/packages/web-util/src/forms/forms.ts
+++ b/packages/web-util/src/forms/forms.ts
@@ -14,9 +14,12 @@ import { InputText } from "./InputText.js";
import { InputTextArea } from "./InputTextArea.js";
import { InputToggle } from "./InputToggle.js";
import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
-import { InternationalizationAPI, UIFieldElementDescription } from "../index.browser.js";
+import {
+ InternationalizationAPI,
+ UIFieldElementDescription,
+} from "../index.browser.js";
import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
-import {UIFormFieldBaseConfig, UIFormElementConfig} from "./ui-form.js";
+import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js";
/**
* Constrain the type with the ui props
*/
@@ -148,11 +151,11 @@ export function RenderAllFieldsByUiConfig({
/**
* convert field configuration to render function
- *
- * @param i18n_
- * @param fieldConfig
- * @param form
- * @returns
+ *
+ * @param i18n_
+ * @param fieldConfig
+ * @param form
+ * @returns
*/
export function convertUiField(
i18n_: InternationalizationAPI,
@@ -175,7 +178,12 @@ export function convertUiField(
type: config.type,
properties: {
...converBaseFieldsProps(i18n_, config),
- fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ fields: convertUiField(
+ i18n_,
+ config.fields,
+ form,
+ getConverterById,
+ ),
},
};
return resp;
@@ -190,7 +198,12 @@ export function convertUiField(
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
labelField: config.labelFieldId,
- fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ fields: convertUiField(
+ i18n_,
+ config.fields,
+ form,
+ getConverterById,
+ ),
},
} as UIFormField;
}
@@ -208,8 +221,8 @@ export function convertUiField(
type: "amount",
properties: {
...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- currency: config.currency,
+ ...converInputFieldsProps(form, config, getConverterById),
+ currency: config.currency,
},
} as UIFormField;
}
@@ -230,11 +243,10 @@ export function convertUiField(
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
choices: config.choices,
-
},
- }as UIFormField;
+ } as UIFormField;
}
- case "file":{
+ case "file": {
return {
type: "file",
properties: {
@@ -245,7 +257,7 @@ export function convertUiField(
},
} as UIFormField;
}
- case "integer":{
+ case "integer": {
return {
type: "integer",
properties: {
@@ -254,7 +266,7 @@ export function convertUiField(
},
} as UIFormField;
}
- case "selectMultiple":{
+ case "selectMultiple": {
return {
type: "selectMultiple",
properties: {
@@ -285,7 +297,7 @@ export function convertUiField(
}
case "textArea": {
return {
- type: "text",
+ type: "textArea",
properties: {
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
@@ -308,30 +320,27 @@ export function convertUiField(
});
}
-
-
function getAddonById(_id: string | undefined): Addon {
return undefined!;
}
-
type GetConverterById = (
id: string | undefined,
config: unknown,
) => StringConverter<unknown>;
-
function converInputFieldsProps(
form: object,
p: UIFormFieldBaseConfig,
getConverterById: GetConverterById,
) {
+ const names = p.id.split(".");
return {
converter: getConverterById(p.converterId, p),
- handler: getValueDeeper2(form, p.id.split(".")),
- name: p.name,
+ handler: getValueDeeper2(form, names),
required: p.required,
disabled: p.disabled,
+ name: names[names.length - 1],
help: p.help,
placeholder: p.placeholder,
tooltip: p.tooltip,
@@ -347,7 +356,6 @@ function converBaseFieldsProps(
after: getAddonById(p.addonAfterId),
before: getAddonById(p.addonBeforeId),
hidden: p.hidden,
- name: p.name,
help: i18n_.str`${p.help}`,
label: i18n_.str`${p.label}`,
tooltip: i18n_.str`${p.tooltip}`,
@@ -368,5 +376,3 @@ export function getValueDeeper2(
}
return getValueDeeper2(object[head], rest);
}
-
-
diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts
index 012499d6d..f26e08f3b 100644
--- a/packages/web-util/src/forms/ui-form.ts
+++ b/packages/web-util/src/forms/ui-form.ts
@@ -12,7 +12,9 @@ import {
codecOptional,
Integer,
TalerProtocolTimestamp,
+ TranslatedString,
} from "@gnu-taler/taler-util";
+import { InternationalizationAPI } from "../index.browser.js";
export type FormConfiguration = DoubleColumnForm;
@@ -134,9 +136,6 @@ export type UIFieldElementDescription = {
/* short text to be shown close to the field, usually below and dimmer*/
help?: string;
- /* name of the field, useful for a11y */
- name: string;
-
/* if the field should be initially hidden */
hidden?: boolean;
@@ -162,6 +161,11 @@ export type UIFormFieldBaseConfig = UIFieldElementDescription & {
*/
converterId?: string;
+ /* return an error message if the value is not valid, returns un undefined
+ if there is no error
+ */
+ validator?: (value: string) => TranslatedString | undefined;
+
/* property id of the form */
id: UIHandlerId;
};
@@ -181,7 +185,6 @@ const codecForUIFormFieldBaseDescriptionTemplate = <
.property("hidden", codecOptional(codecForBoolean()))
.property("help", codecOptional(codecForString()))
.property("label", codecForString())
- .property("name", codecForString())
.property("tooltip", codecOptional(codecForString()));
const codecForUIFormFieldBaseConfigTemplate = <
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 103b88c86..929c54a58 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -155,7 +155,7 @@ function errorMap<T extends OperationFail<unknown>>(
notify({
type: "error",
title: map(resp.case),
- description: resp.detail.hint as TranslatedString,
+ description: (resp.detail?.hint as TranslatedString) ?? "",
debug: resp.detail,
when: AbsoluteTime.now(),
});
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 2f3b57b8d..66295f649 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -7,4 +7,5 @@ export * from "./utils/route.js";
export * from "./context/index.js";
export * from "./components/index.js";
export * from "./forms/index.js";
+export { encodeCrockForURI, decodeCrockFromURI } from "./utils/base64.js";
export { renderStories, parseGroupImport } from "./stories.js";
diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts
index 2260ecb9a..f912c6538 100644
--- a/packages/web-util/src/index.build.ts
+++ b/packages/web-util/src/index.build.ts
@@ -292,6 +292,7 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions {
entryPoints: params.source.js,
publicPath: params.public,
outdir: params.destination,
+ treeShaking: true,
minify: false, //params.type === "production",
sourcemap: true, //params.type !== "production",
define: {
diff --git a/packages/web-util/src/live-reload.ts b/packages/web-util/src/live-reload.ts
index cd3a7540d..c89d09383 100644
--- a/packages/web-util/src/live-reload.ts
+++ b/packages/web-util/src/live-reload.ts
@@ -1,10 +1,12 @@
/* eslint-disable no-undef */
function setupLiveReload(): void {
- const stopWs = localStorage.getItem("stop-ws")
+ const stopWs = localStorage.getItem("stop-ws");
if (!!stopWs) return;
const protocol = window.location.protocol === "http:" ? "ws:" : "wss:";
- const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`);
+ const ws = new WebSocket(
+ `${protocol}//${window.location.hostname}:${window.location.port}/ws`,
+ );
ws.addEventListener("message", (message) => {
try {
@@ -60,18 +62,22 @@ setupLiveReload();
function showReloadOverlay(): void {
const d = document.createElement("div");
d.id = "overlay";
- d.style.position = "absolute";
- d.style.width = "100%";
- d.style.height = "100%";
+ d.style.position = "fixed";
+ d.style.left = "0px";
+ d.style.top = "0px";
+ d.style.width = "100vw";
+ d.style.height = "100vh";
+ d.style.display = "flex";
+ d.style.alignItems = "center";
+ d.style.justifyContent = "center";
+ d.style.fontFamily = `system-ui, -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif`;
d.style.color = "white";
d.style.backgroundColor = "rgba(0,0,0,0.5)";
- d.style.display = "flex";
d.style.zIndex = String(Number.MAX_SAFE_INTEGER);
- d.style.justifyContent = "center";
const h = document.createElement("h1");
h.id = "overlay-text";
h.style.margin = "auto";
- h.innerHTML = "reloading...";
+ h.innerHTML = "Reloading...";
d.appendChild(h);
if (document.body.firstChild) {
document.body.insertBefore(d, document.body.firstChild);
diff --git a/packages/web-util/src/utils/base64.ts b/packages/web-util/src/utils/base64.ts
index 0e075880f..e51591df6 100644
--- a/packages/web-util/src/utils/base64.ts
+++ b/packages/web-util/src/utils/base64.ts
@@ -14,13 +14,25 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
+
+const utf8Encoder = new TextEncoder();
+const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+
+export function encodeCrockForURI(string: string): string {
+ return encodeCrock(utf8Encoder.encode(string));
+}
+
+export function decodeCrockFromURI(enc: string): string {
+ return utf8Decoder.decode(decodeCrock(enc));
+}
export function base64encode(str: string): string {
- return base64EncArr(strToUTF8Arr(str))
+ return base64EncArr(strToUTF8Arr(str));
}
export function base64decode(str: string): string {
- return UTF8ArrToStr(base64DecToArr(str))
+ return UTF8ArrToStr(base64DecToArr(str));
}
// from https://developer.mozilla.org/en-US/docs/Glossary/Base64
@@ -103,7 +115,7 @@ function base64EncArr(aBytes: Uint8Array): string {
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
- uint6ToB64(nUint24 & 63)
+ uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
@@ -114,8 +126,13 @@ function base64EncArr(aBytes: Uint8Array): string {
);
}
-/* UTF-8 array to JS string and vice versa */
-
+/**
+ * UTF-8 array to JS string and vice versa
+ *
+ * @param aBytes
+ * @deprecated use textEncoder
+ * @returns
+ */
function UTF8ArrToStr(aBytes: Uint8Array): string {
let sView = "";
let nPart;
@@ -125,40 +142,46 @@ function UTF8ArrToStr(aBytes: Uint8Array): string {
sView += String.fromCodePoint(
nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
- (nPart - 252) * 1073741824 +
- ((aBytes[++nIdx] - 128) << 24) +
- ((aBytes[++nIdx] - 128) << 18) +
- ((aBytes[++nIdx] - 128) << 12) +
- ((aBytes[++nIdx] - 128) << 6) +
- aBytes[++nIdx] -
- 128
+ (nPart - 252) * 1073741824 +
+ ((aBytes[++nIdx] - 128) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
: nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
? ((nPart - 248) << 24) +
- ((aBytes[++nIdx] - 128) << 18) +
- ((aBytes[++nIdx] - 128) << 12) +
- ((aBytes[++nIdx] - 128) << 6) +
- aBytes[++nIdx] -
- 128
- : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
- ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
- : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
- ? ((nPart - 224) << 12) +
+ : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
+ ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
+ : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
+ ? ((nPart - 224) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
: nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
: /* nPart < 127 ? */ /* one byte */
- nPart
+ nPart,
);
}
return sView;
}
+/**
+ *
+ * @param sDOMStr
+ * @deprecated use textEncoder
+ * @returns
+ */
function strToUTF8Arr(sDOMStr: string): Uint8Array {
let nChr;
const nStrLen = sDOMStr.length;
@@ -168,7 +191,9 @@ function strToUTF8Arr(sDOMStr: string): Uint8Array {
for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
nChr = sDOMStr.codePointAt(nMapIdx);
if (nChr === undefined) {
- throw Error(`No char at ${nMapIdx} on string with length: ${sDOMStr.length}`)
+ throw Error(
+ `No char at ${nMapIdx} on string with length: ${sDOMStr.length}`,
+ );
}
if (nChr >= 0x10000) {
@@ -197,7 +222,9 @@ function strToUTF8Arr(sDOMStr: string): Uint8Array {
while (nIdx < nArrLen) {
nChr = sDOMStr.codePointAt(nChrIdx);
if (nChr === undefined) {
- throw Error(`No char at ${nChrIdx} on string with length: ${sDOMStr.length}`)
+ throw Error(
+ `No char at ${nChrIdx} on string with length: ${sDOMStr.length}`,
+ );
}
if (nChr < 128) {
/* one byte */
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 9c820bb4b..2f7f24fd6 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -21,7 +21,7 @@ import {
Duration,
RequestThrottler,
TalerError,
- TalerErrorCode
+ TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
@@ -85,7 +85,9 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
}
const myBody: ArrayBuffer | undefined =
- requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH"
+ requestMethod === "POST" ||
+ requestMethod === "PUT" ||
+ requestMethod === "PATCH"
? encodeBody(requestBody)
: undefined;
@@ -93,8 +95,19 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
if (requestHeader) {
Object.entries(requestHeader).forEach(([key, value]) => {
if (value === undefined) return;
- requestHeadersMap[key] = value
- })
+ requestHeadersMap[key] = value;
+ });
+ }
+
+ /**
+ * default header assume everything is json
+ * in case of formData the content-type will be
+ * auto generated
+ */
+ if (requestBody instanceof FormData) {
+ delete requestHeadersMap["Content-Type"]
+ } else if (requestBody instanceof URLSearchParams) {
+ requestHeadersMap["Content-Type"] = "application/x-www-form-urlencoded"
}
const controller = new AbortController();
@@ -106,7 +119,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
}
if (requestCancel) {
requestCancel.onCancelled(() => {
- controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)
+ controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR);
});
}
@@ -116,7 +129,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
body: myBody,
method: requestMethod,
signal: controller.signal,
- redirect: requestRedirect
+ redirect: requestRedirect,
});
if (timeoutId) {
@@ -127,13 +140,15 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
response.headers.forEach((value, key) => {
headerMap.set(key, value);
});
+ const text = makeTextHandler(response, requestUrl, requestMethod);
+ const json = makeJsonHandler(response, requestUrl, requestMethod, text);
return {
headers: headerMap,
status: response.status,
requestMethod,
requestUrl,
- json: makeJsonHandler(response, requestUrl, requestMethod),
- text: makeTextHandler(response, requestUrl, requestMethod),
+ json,
+ text,
bytes: async () => (await response.blob()).arrayBuffer(),
};
} catch (e) {
@@ -143,7 +158,8 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
{
requestUrl,
requestMethod,
- timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms
+ timeoutMs:
+ requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms,
},
`HTTP request failed.`,
);
@@ -151,7 +167,6 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
throw e;
}
}
-
}
function makeTextHandler(
@@ -159,20 +174,29 @@ function makeTextHandler(
requestUrl: string,
requestMethod: string,
) {
- return async function getTextFromResponse(): Promise<any> {
- let respText;
- try {
- respText = await response.text();
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- requestMethod,
- httpStatusCode: response.status,
- },
- "Invalid text from HTTP response",
- );
+ let firstTime = true;
+ let respText: string;
+ let error: TalerError | undefined;
+ return async function getTextFromResponse(): Promise<string> {
+ if (firstTime) {
+ firstTime = false;
+ try {
+ respText = await response.text();
+ } catch (e) {
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
+ },
+ "Invalid text from HTTP response",
+ );
+ }
+ }
+ if (error !== undefined) {
+ throw error;
}
return respText;
};
@@ -182,35 +206,70 @@ function makeJsonHandler(
response: Response,
requestUrl: string,
requestMethod: string,
+ readTextHandler: () => Promise<string>,
) {
- let responseJson: unknown = undefined;
+ let firstTime = true;
+ let responseJson: string | undefined = undefined;
+ let error: TalerError | undefined;
return async function getJsonFromResponse(): Promise<any> {
- if (responseJson === undefined) {
+ if (firstTime) {
+ let responseText: string;
try {
- responseJson = await response.json();
+ responseText = await readTextHandler();
} catch (e) {
- const message = e instanceof Error ? `Invalid JSON from HTTP response: ${e.message}` : "Invalid JSON from HTTP response"
- throw TalerError.fromDetail(
+ const message =
+ e instanceof Error
+ ? `Couldn't read HTTP response: ${e.message}`
+ : "Couldn't read HTTP response";
+ error = TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl,
requestMethod,
httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
},
message,
);
}
+ if (!error) {
+ try {
+ // @ts-expect-error no error then text is initialized
+ responseJson = JSON.parse(responseText);
+ } catch (e) {
+ const message =
+ e instanceof Error
+ ? `Invalid JSON from HTTP response: ${e.message}`
+ : "Invalid JSON from HTTP response";
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ // @ts-expect-error no error then text is initialized
+ response: responseText,
+ httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
+ },
+ message,
+ );
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ response: JSON.stringify(responseJson),
+ httpStatusCode: response.status,
+ },
+ "Invalid JSON from HTTP response: null or not object",
+ );
+ }
+ }
}
- if (responseJson === null || typeof responseJson !== "object") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- requestMethod,
- httpStatusCode: response.status,
- },
- "Invalid JSON from HTTP response: null or not object",
- );
+ if (error !== undefined) {
+ throw error;
}
return responseJson;
};
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 23d3af468..0c11c8c8a 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -28,8 +28,6 @@ export enum ErrorType {
UNEXPECTED,
}
-
-
/**
*
* @param baseUrl URL where the service is located
@@ -51,8 +49,11 @@ export async function defaultRequestHandler<T>(
`${options.basicAuth.username}:${options.basicAuth.password}`,
)}`;
}
+
requestHeaders["Content-Type"] =
- !options.contentType || options.contentType === "json" ? "application/json" : "text/plain";
+ !options.contentType || options.contentType === "json"
+ ? "application/json"
+ : "text/plain";
if (options.talerAmlOfficerSignature) {
requestHeaders["Taler-AML-Officer-Signature"] =
@@ -82,7 +83,7 @@ export async function defaultRequestHandler<T>(
loading: false,
message: `invalid URL: "${baseUrl}${endpoint}"`,
};
- throw new RequestError(error)
+ throw new RequestError(error);
}
Object.entries(requestParams).forEach(([key, value]) => {
@@ -113,7 +114,7 @@ export async function defaultRequestHandler<T>(
loading: false,
message: `unsupported request body type: "${typeof requestBody}"`,
};
- throw new RequestError(error)
+ throw new RequestError(error);
}
}
@@ -158,7 +159,7 @@ export async function defaultRequestHandler<T>(
type: ErrorType.UNEXPECTED,
exception: ex,
loading: false,
- message: (ex instanceof Error ? ex.message : ""),
+ message: ex instanceof Error ? ex.message : "",
};
throw new RequestError(error);
}
@@ -469,9 +470,8 @@ export function buildRequestFailed<ErrorDetail>(
*/
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
try {
- return new URL(`${baseUrl}${endpoint}`)
+ return new URL(`${baseUrl}${endpoint}`);
} catch (ex) {
- return undefined
+ return undefined;
}
-
-} \ No newline at end of file
+}
diff --git a/packages/web-util/src/utils/route.ts b/packages/web-util/src/utils/route.ts
index 494a61efa..fbbbfebd1 100644
--- a/packages/web-util/src/utils/route.ts
+++ b/packages/web-util/src/utils/route.ts
@@ -25,7 +25,13 @@ export type AppLocation = string & {
};
export type EmptyObject = Record<string, never>;
-
+/**
+ * FIXME: receive parameters
+ * maybe return URL for reverse function instead of string
+ * @param pattern
+ * @param reverse
+ * @returns
+ */
export function urlPattern<
T extends Record<string, string | undefined> = EmptyObject,
>(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
@@ -75,7 +81,7 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
pageList: Array<keyof T>,
path: string,
params: Record<string, string[]>,
-): Location<T> | undefined {
+): Location<T> | LocationNotFound<T> {
for (let idx = 0; idx < pageList.length; idx++) {
const name = pageList[idx];
const found = pagesMap[name].pattern.exec(path);
@@ -92,7 +98,8 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
return { name, parent: pagesMap, values, params };
}
}
- return undefined;
+ // @ts-expect-error values is a map string which is equivalent to the RouteParamsType
+ return { name: undefined, parent: pagesMap, values: {}, params };
}
/**
@@ -109,13 +116,13 @@ type RouteParamsType<
*/
type MapKeyValue<Type> = {
[Key in keyof Type]: Key extends string
- ? {
- parent: Type;
- name: Key;
- values: RouteParamsType<Type, Key>;
- params: Record<string, string[]>;
- }
- : never;
+ ? {
+ parent: Type;
+ name: Key;
+ values: RouteParamsType<Type, Key>;
+ params: Record<string, string[]>;
+ }
+ : never;
};
/**
@@ -124,3 +131,9 @@ type MapKeyValue<Type> = {
type EnumerationOf<T> = T[keyof T];
export type Location<T> = EnumerationOf<MapKeyValue<T>>;
+export type LocationNotFound<Type> = {
+ parent: Type;
+ name: undefined;
+ values: RouteParamsType<Type, keyof Type>;
+ params: Record<string, string[]>;
+};