aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/aml-backoffice-ui/package.json2
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-webui/package.json2
-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.json2
-rw-r--r--packages/auditor-backoffice-ui/src/AdminRoutes.tsx53
-rw-r--r--packages/auditor-backoffice-ui/src/Application.tsx247
-rw-r--r--packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx190
-rw-r--r--packages/auditor-backoffice-ui/src/InstanceRoutes.tsx875
-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.tsx42
-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/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/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/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/JumpToElementById.tsx59
-rw-r--r--packages/auditor-backoffice-ui/src/components/form/TextField.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/FormProvider.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/Input.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/Input.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/InputCurrency.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/InputNumber.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/InputSelector.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/InputToggle.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/forms/useField.tsx (renamed from packages/auditor-backoffice-ui/src/components/form/useField.tsx)2
-rw-r--r--packages/auditor-backoffice-ui/src/components/index.stories.ts17
-rw-r--r--packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/LangSelector.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/NavigationBar.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/SideBar.tsx267
-rw-r--r--packages/auditor-backoffice-ui/src/components/menu/index.tsx411
-rw-r--r--packages/auditor-backoffice-ui/src/components/modal/index.tsx12
-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.ts60
-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.ts1978
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/async.ts77
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/backend.ts620
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/bank.ts217
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/critical.ts70
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts161
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/entity.ts82
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/finance.ts61
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/index.ts176
-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.ts83
-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/index.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx57
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx257
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx74
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx82
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx52
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx287
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx110
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx140
-rw-r--r--packages/auditor-backoffice-ui/src/paths/default/Table.tsx155
-rw-r--r--packages/auditor-backoffice-ui/src/paths/default/index.tsx130
-rw-r--r--packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx346
-rw-r--r--packages/auditor-backoffice-ui/src/paths/details/index.tsx (renamed from packages/auditor-backoffice-ui/src/context/instance.ts)27
-rw-r--r--packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx214
-rw-r--r--packages/auditor-backoffice-ui/src/paths/finance/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/index.tsx)71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx173
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/create/index.tsx65
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/Table.tsx385
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/list/index.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx195
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/accounts/update/index.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/create/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/details/DetailPage.tsx83
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/index.tsx87
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/details/stories.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts19
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx58
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx208
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx63
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx705
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/create/index.tsx114
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx135
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx770
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx129
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/details/index.tsx95
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx107
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx226
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/Table.tsx417
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/orders/list/index.tsx231
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.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/create/index.tsx70
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx64
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx211
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx106
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx32
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx186
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx80
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/products/create/index.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/reserves/create/Create.stories.tsx43
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx277
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx120
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx190
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/create/index.tsx71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx266
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx126
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx94
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx124
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx102
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx96
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/Table.tsx320
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/reserves/list/index.tsx171
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx28
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/templates/create/CreatePage.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/Update.stories.tsx32
-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/create/index.tsx68
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx93
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx134
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/Table.tsx229
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/transfers/list/index.tsx118
-rw-r--r--packages/auditor-backoffice-ui/src/paths/instance/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/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.tsx319
-rw-r--r--packages/auditor-backoffice-ui/src/paths/notfound/index.tsx18
-rw-r--r--packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx72
-rw-r--r--packages/auditor-backoffice-ui/src/paths/operations/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx)71
-rw-r--r--packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx70
-rw-r--r--packages/auditor-backoffice-ui/src/paths/security/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/templates/qr/index.tsx)58
-rw-r--r--packages/auditor-backoffice-ui/src/paths/settings/index.tsx55
-rw-r--r--packages/auditor-backoffice-ui/src/scss/_aside.scss2
-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/sw.js25
-rw-r--r--packages/auditor-backoffice-ui/src/utils/amount.ts9
-rw-r--r--packages/auditor-backoffice-ui/src/utils/constants.ts2
-rw-r--r--packages/auditor-backoffice-ui/src/utils/regex.test.ts2
-rw-r--r--packages/auditor-backoffice-ui/src/utils/table.ts14
-rw-r--r--packages/auditor-backoffice-ui/src/utils/types.ts2
-rw-r--r--packages/bank-ui/package.json2
-rw-r--r--packages/bank-ui/src/hooks/account.ts2
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts26
-rw-r--r--packages/bank-ui/src/i18n/es.po8
-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.ts2
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts14
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx104
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.tsx2
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx28
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx15
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx19
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx23
-rw-r--r--packages/bank-ui/src/pages/WireTransfer.tsx7
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx39
-rw-r--r--packages/bank-ui/src/pages/WithdrawalQRCode.tsx17
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx208
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx7
-rw-r--r--packages/bank-ui/src/settings.json2
-rw-r--r--packages/bank-ui/src/settings.ts7
-rw-r--r--packages/challenger-ui/package.json2
-rw-r--r--packages/challenger-ui/src/Routing.tsx4
-rw-r--r--packages/challenger-ui/src/app.tsx5
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx4
-rw-r--r--packages/challenger-ui/src/context/preferences.ts87
-rw-r--r--packages/challenger-ui/src/declaration.d.ts (renamed from packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx)29
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts2
-rw-r--r--packages/challenger-ui/src/hooks/session.ts18
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx128
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx60
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx151
-rw-r--r--packages/idb-bridge/package.json2
-rw-r--r--packages/idb-bridge/src/util/errors.ts2
-rw-r--r--packages/merchant-backend-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/package.json2
-rw-r--r--packages/merchant-backoffice-ui/src/Routing.tsx175
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx118
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx20
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx100
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx22
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx127
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx140
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts1704
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/bank.ts26
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/preference.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/tokenfamily.ts78
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/es.po296
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx41
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx215
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx110
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx473
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx52
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx314
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx126
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx29
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/products/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/deposit_confirmations/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/webhooks/create/index.tsx)24
-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.tsx143
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx172
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx (renamed from packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx)81
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx15
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx95
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts54
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss16
-rw-r--r--packages/merchant-backoffice-ui/src/utils/table.ts2
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/taler-harness/debian/changelog24
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/bench2.ts1
-rw-r--r--packages/taler-harness/src/harness/harness.ts25
-rw-r--r--packages/taler-harness/src/harness/helpers.ts14
-rw-r--r--packages/taler-harness/src/index.ts48
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts114
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-categories.ts162
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-pull-large.ts11
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cli-termination.ts67
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts96
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts84
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-external.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-flex.ts19
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts8
-rw-r--r--packages/taler-util/package.json2
-rw-r--r--packages/taler-util/src/errors.ts27
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts8
-rw-r--r--packages/taler-util/src/http-client/challenger.ts46
-rw-r--r--packages/taler-util/src/http-client/merchant.ts6
-rw-r--r--packages/taler-util/src/http-client/types.ts191
-rw-r--r--packages/taler-util/src/http-common.ts12
-rw-r--r--packages/taler-util/src/http-impl.node.ts7
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts17
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-util/src/notifications.ts16
-rw-r--r--packages/taler-util/src/operation.ts51
-rw-r--r--packages/taler-util/src/payto.test.ts15
-rw-r--r--packages/taler-util/src/payto.ts77
-rw-r--r--packages/taler-util/src/qr.ts166
-rw-r--r--packages/taler-util/src/taler-error-codes.ts32
-rw-r--r--packages/taler-util/src/taler-types.ts3
-rw-r--r--packages/taler-util/src/taleruri.test.ts12
-rw-r--r--packages/taler-util/src/taleruri.ts10
-rw-r--r--packages/taler-util/src/transactions-types.ts5
-rw-r--r--packages/taler-util/src/url.ts2
-rw-r--r--packages/taler-util/src/wallet-types.ts120
-rw-r--r--packages/taler-wallet-cli/debian/changelog18
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts47
-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/coinSelection.ts373
-rw-r--r--packages/taler-wallet-core/src/common.ts106
-rw-r--r--packages/taler-wallet-core/src/db.ts165
-rw-r--r--packages/taler-wallet-core/src/dbless.ts44
-rw-r--r--packages/taler-wallet-core/src/deposits.ts135
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts404
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts388
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts77
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts50
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts42
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts48
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts145
-rw-r--r--packages/taler-wallet-core/src/query.ts112
-rw-r--r--packages/taler-wallet-core/src/recoup.ts39
-rw-r--r--packages/taler-wallet-core/src/refresh.ts78
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts118
-rw-r--r--packages/taler-wallet-core/src/testing.ts12
-rw-r--r--packages/taler-wallet-core/src/transactions.ts21
-rw-r--r--packages/taler-wallet-core/src/versions.ts7
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts68
-rw-r--r--packages/taler-wallet-core/src/wallet.ts1520
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts182
-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/package.json2
-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/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.tsx90
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx158
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts1
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts11
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts1
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts5
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts7
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts5
-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.ts16
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts58
-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.tsx21
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po10
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts1
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts62
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts5
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts145
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts44
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts65
-rw-r--r--packages/web-util/package.json2
-rw-r--r--packages/web-util/src/context/translation.ts2
-rw-r--r--packages/web-util/src/utils/request.ts1
441 files changed, 12530 insertions, 32001 deletions
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
index 9c33862f7..c3549ef52 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index 40bdb927e..47d1505d1 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.12.2",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index c987f0ceb..c89b8eecc 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.12.2",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 9f56489d1..17e8e74fc 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.11.4",
+ "version": "0.12.2",
"license": "MIT",
"type": "module",
"scripts": {
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..bbebabf39 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
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..3b6aa8dd3 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,149 +17,150 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
+import {HttpStatusCode, LibtoolVersion} from "@gnu-taler/taler-util";
import {
- ErrorType,
- TranslationProvider,
- useTranslationContext,
+ ErrorType,
+ TranslationProvider,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useMemo } from "preact/hooks";
-import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
-import { Loading } from "./components/exception/loading.js";
+import {Fragment, VNode, h, render} from "preact";
+import {useMemo} from "preact/hooks";
+import {ApplicationReadyRoutes} from "./ApplicationReadyRoutes.js";
+import {Loading} from "./components/exception/loading.js";
import {
- NotConnectedAppMenu,
- NotificationCard
+ NotConnectedAppMenu,
+ NotificationCard
} from "./components/menu/index.js";
import {
- BackendContextProvider
+ BackendContextProvider
} from "./context/backend.js";
-import { ConfigContextProvider } from "./context/config.js";
-import { useBackendConfig } from "./hooks/backend.js";
+import {ConfigContextProvider} from "./context/config.js";
+import {useBackendConfig} from "./hooks/backend.js";
import { strings } from "./i18n/strings.js";
export function Application(): VNode {
- return (
- <BackendContextProvider>
- <TranslationProvider source={strings}>
- <ApplicationStatusRoutes />
- </TranslationProvider>
- </BackendContextProvider>
- );
+ return (
+ <BackendContextProvider>
+ <TranslationProvider source={strings}>
+ <ApplicationStatusRoutes/>
+ </TranslationProvider>
+ </BackendContextProvider>
+ );
}
/**
* Check connection testing against /config
- *
- * @returns
+ *
+ * @returns
*/
function ApplicationStatusRoutes(): VNode {
- const result = useBackendConfig();
- const { i18n } = useTranslationContext();
+ 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 />;
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- ) {
- return (
- <Fragment>
- <NotConnectedAppMenu title="Login" />
- <NotificationCard
- notification={{
- message: i18n.str`Checking the /config endpoint got authorization error`,
- type: "ERROR",
- description: `The /config endpoint of the backend server should be accessible`,
- }}
- />
- </Fragment>
- );
+ if (!result.ok) {
+ if (result.loading) return <Loading/>;
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ ) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Login"/>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Checking the /config endpoint got authorization error`,
+ type: "ERROR",
+ description: `The /config endpoint of the backend server should be accessible`,
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ ) {
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Error"/>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Could not find /config endpoint on this URL`,
+ type: "ERROR",
+ description: `Check the URL or contact the system administrator.`,
+ }}
+ />
+ </Fragment>
+ );
+ }
+ if (result.type === ErrorType.SERVER) {
+ <Fragment>
+ <NotConnectedAppMenu title="Error"/>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Server response with an error code`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>;
+ }
+ if (result.type === ErrorType.UNREADABLE) {
+ <Fragment>
+ <NotConnectedAppMenu title="Error"/>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>;
+ }
+ return (
+ <Fragment>
+ <NotConnectedAppMenu title="Error"/>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Unexpected Error`,
+ type: "ERROR",
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
+ }}
+ />
+ </Fragment>
+ );
}
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- ) {
- return (
- <Fragment>
- <NotConnectedAppMenu title="Error" />
- <NotificationCard
- notification={{
- message: i18n.str`Could not find /config endpoint on this URL`,
- type: "ERROR",
- description: `Check the URL or contact the system administrator.`,
- }}
- />
+
+ 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>
- );
- }
- if (result.type === ErrorType.SERVER) {
- <Fragment>
- <NotConnectedAppMenu title="Error" />
- <NotificationCard
- notification={{
- message: i18n.str`Server response with an error code`,
- type: "ERROR",
- description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
- }}
- />
- </Fragment>;
- }
- if (result.type === ErrorType.UNREADABLE) {
- <Fragment>
- <NotConnectedAppMenu title="Error" />
- <NotificationCard
- notification={{
- message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
- type: "ERROR",
- description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
- }}
- />
- </Fragment>;
}
+
return (
- <Fragment>
- <NotConnectedAppMenu title="Error" />
- <NotificationCard
- notification={{
- message: i18n.str`Unexpected Error`,
- type: "ERROR",
- description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
- }}
- />
- </Fragment>
+ <div class="has-navbar-fixed-top">
+ <ConfigContextProvider value={ctx!}>
+ <ApplicationReadyRoutes/>
+ </ConfigContextProvider>
+ </div>
);
- }
-
- 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>
- }
-
- return (
- <div class="has-navbar-fixed-top">
- <ConfigContextProvider value={ctx}>
- <ApplicationReadyRoutes />
- </ConfigContextProvider>
- </div>
- );
-}
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 414eee39d..576792d6f 100644
--- a/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/auditor-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -17,159 +17,73 @@
/**
*
* @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 { useState } from "preact/hooks";
-import { InstanceRoutes } from "./InstanceRoutes.js";
+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 {useEffect, useErrorBoundary, useState} from "preact/hooks";
+import {InstanceRoutes} from "./InstanceRoutes.js";
import {
- NotConnectedAppMenu,
- NotYetReadyAppMenu,
- NotificationCard,
+ 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 { useBackendContext, useBackendTokenContext } from "./context/backend.js";
+import {Settings} from "./paths/settings/index.js";
+import { useBackendConfig, useBackendToken } from "./hooks/backend.js";
+import { Loading } from "./components/exception/loading.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)
- }
-
- const result = useBackendInstancesTestForAdmin();
-
- const clearTokenAndGoToRoot = () => {
- route("/");
- };
- const [showSettings, setShowSettings] = useState(false)
- const unauthorizedAdmin = !result.loading
- && !result.ok
- && result.type === ErrorType.CLIENT
- && result.status === HttpStatusCode.Unauthorized;
-
- if (!alreadyTriedLogin && !result.ok) {
- return (
- <Fragment>
- <NotConnectedAppMenu title="Welcome!" />
- <LoginPage onConfirm={updateToken} />
- </Fragment>
- );
- }
-
- if (showSettings) {
- return <Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
- <Settings onClose={() => setShowSettings(false)} />
- </Fragment>
- }
-
- if (result.loading) {
- return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />;
- }
-
- let admin = result.ok || unauthorizedAdmin;
- let instanceNameByBackendURL: string | undefined;
+ const {i18n} = useTranslationContext();
+ const [unauthorized, setUnauthorized] = useState(false)
+ const [backendToken, setToken] = useState(false)
+ const { url: backendURL} = useBackendContext();
+ const { token } = useBackendTokenContext();
+
+ //TODO FIX bearer
+ const result = useBackendToken();
+ if (result.loading) return <Loading/>;
+ if (!result.ok) {
+ return (
+ <LoginPage />
+ );
+ }
+ const [showSettings, setShowSettings] = useState(false)
- 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} /> */}
+ if (showSettings) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings"/>
+ <Settings onClose={() => setShowSettings(false)}/>
</Fragment>
- );
}
- instanceNameByBackendURL = match[1];
- }
-
- if (unauthorized || unauthorizedAdmin) {
- return <Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`Check your token is valid`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- }
-
- const history = createHashHistory();
- return (
- <Router history={history}>
- <Route
- default
- component={DefaultMainRoute}
- admin={admin}
- onUnauthorized={() => setUnauthorized(true)}
- onLoginPass={() => {
- setUnauthorized(false)
- }}
- instanceNameByBackendURL={instanceNameByBackendURL}
- />
- </Router>
- );
+ const history = createHashHistory();
+ return (
+ <Router history={history}>
+ <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",
- );
+ url, //from preact-router
+ }: any): VNode {
+ //TODO
+ url = "app/#" + url;
- 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..83c1c9f4d 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,469 +17,456 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
- * @author Nic Eigel
+ * @author Nicola Eigel
*/
+import {TranslatedString} from "@gnu-taler/taler-util";
import {
- useTranslationContext,
- HttpError,
- ErrorType,
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { Fragment, FunctionComponent, h, VNode } from "preact";
-import { Route, route, Router } from "preact-router";
-import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
-import { Loading } from "./components/exception/loading.js";
-import { Menu, NotificationCard } from "./components/menu/index.js";
-import { useBackendContext } from "./context/backend.js";
-import { InstanceContextProvider } from "./context/instance.js";
-import {
- useBackendDefaultToken,
- useBackendInstanceToken,
- useSimpleLocalStorage,
-} from "./hooks/index.js";
-import { useInstanceKYCDetails } from "./hooks/instance.js";
-import InstanceCreatePage from "./paths/admin/create/index.js";
-import InstanceListPage from "./paths/admin/list/index.js";
-import TokenPage from "./paths/instance/token/index.js";
-import ListKYCPage from "./paths/instance/kyc/list/index.js";
-import OrderCreatePage from "./paths/instance/orders/create/index.js";
-import OrderDetailsPage from "./paths/instance/orders/details/index.js";
-import OrderListPage from "./paths/instance/orders/list/index.js";
-import DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js";
-import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js";
-import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/update/index.js";
-import ProductCreatePage from "./paths/instance/products/create/index.js";
-import ProductListPage from "./paths/instance/products/list/index.js";
-import ProductUpdatePage from "./paths/instance/products/update/index.js";
-import BankAccountCreatePage from "./paths/instance/accounts/create/index.js";
-import BankAccountListPage from "./paths/instance/accounts/list/index.js";
-import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js";
-import ReservesCreatePage from "./paths/instance/reserves/create/index.js";
-import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
-import ReservesListPage from "./paths/instance/reserves/list/index.js";
-import TemplateCreatePage from "./paths/instance/templates/create/index.js";
-import TemplateUsePage from "./paths/instance/templates/use/index.js";
-import TemplateQrPage from "./paths/instance/templates/qr/index.js";
-import TemplateListPage from "./paths/instance/templates/list/index.js";
-import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
-import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
-import WebhookListPage from "./paths/instance/webhooks/list/index.js";
-import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
-import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js";
-import ValidatorListPage from "./paths/instance/otp_devices/list/index.js";
-import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js";
-import TransferCreatePage from "./paths/instance/transfers/create/index.js";
-import TransferListPage from "./paths/instance/transfers/list/index.js";
-import InstanceUpdatePage, {
- AdminUpdate as InstanceAdminUpdatePage,
- Props as InstanceUpdatePageProps,
-} from "./paths/instance/update/index.js";
-import { LoginPage } from "./paths/login/index.js";
+import {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 {EntityContextProvider} from "./context/entity.js";
+import {Notification} from "./utils/types.js";
import NotFoundPage from "./paths/notfound/index.js";
-import { Notification } from "./utils/types.js";
-import { LoginToken, MerchantBackend } from "./declaration.js";
-import { Settings } from "./paths/settings/index.js";
-import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
+import {Settings} from "./paths/settings/index.js";
+import DefaultList from "./paths/default/index.js";
+import {
+ AuditorBackend,
+} from "./declaration.js";
+import FinanceDashboard from "./paths/finance/index.js";
+import DetailsDashboard from "./paths/details/index.js";
+import OperationsDashboard from "./paths/operations/index.js";
+import SecurityDashboard from "./paths/security/index.js";
-export enum InstancePaths {
- error = "/error",
- settings = "/settings",
- token = "/token",
+export enum Paths {
+ error = "/error",
+ settings = "/settings",
- 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",
- deposit_confirmation_list = "/deposit-confirmation",
- deposit_confirmation_update = "/deposit-confirmation/:pid/update",
- deposit_confirmation_new = "/deposit-confirmation/new",
+ amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies",
- interface = "/interface",
-}
+ 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",
+
+ purse_list = "/purses",
+
+ refresh_hanging_list = "/refreshes-hanging",
-// eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => { };
+ reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies",
-export enum AdminPaths {
- list_instances = "/instances",
- new_instance = "/instance/new",
- update_instance = "/instance/:id/update",
+ 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"
}
-export interface Props {
- id: string;
- admin?: boolean;
- path: string;
- onUnauthorized: () => void;
- onLoginPass: () => void;
- setInstanceName: (s: string) => void;
+interface TestProps {
+ title: string;
+ endpoint: string;
+ entity: any;
}
-export function InstanceRoutes({
- id,
- admin,
- path,
- // onUnauthorized,
- onLoginPass,
- setInstanceName,
-}: Props): VNode {
- const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
- const [token, updateToken] = useBackendInstanceToken(id);
- const { i18n } = useTranslationContext();
-
- type GlobalNotifState = (Notification & { to: string }) | undefined;
- const [globalNotification, setGlobalNotification] =
- useState<GlobalNotifState>(undefined);
-
- const changeToken = (token?: LoginToken) => {
- if (admin) {
- updateToken(token);
- } else {
- updateDefaultToken(token);
+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};
}
- onLoginPass()
- };
- // const updateLoginStatus = (url: string, token?: string) => {
- // changeToken(token);
- // };
-
- const value = useMemo(
- () => ({ id, token, admin, changeToken }),
- [id, token, admin],
- );
-
- function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
- return function ServerErrorRedirectToImpl(
- error: HttpError<MerchantBackend.ErrorDetail>,
- ) {
- if (error.type === ErrorType.TIMEOUT) {
- setGlobalNotification({
- message: i18n.str`The request to the backend take too long and was cancelled`,
- description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
- type: "ERROR",
- to,
- });
- } else {
- setGlobalNotification({
- message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
- description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
- details:
- error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
- ? error.payload.detail
- : undefined,
- type: "ERROR",
- to,
- });
- }
- return <Redirect to={to} />;
- };
- }
-
- // const LoginPageAccessDeniend = onUnauthorized
- const LoginPageAccessDenied = () => {
- return <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`Session expired or password changed.`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={changeToken} />
- </Fragment>
-
- }
-
- function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
- return function IfAdminCreateDefaultOrImpl(props?: T) {
- if (admin && id === "default") {
- return (
- <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`No 'default' instance configured yet.`,
- description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
- type: "INFO",
- }}
- />
- </Fragment>
- );
- }
- if (props) {
- return <Next {...props} />;
- }
- return <Next />;
- };
- }
-
- const clearTokenAndGoToRoot = () => {
- route("/");
- // clear all tokens
- updateToken(undefined)
- updateDefaultToken(undefined)
- };
-
- return (
- <InstanceContextProvider value={value}>
- <Menu
- instance={id}
- admin={admin}
- onShowSettings={() => {
- route(InstancePaths.interface)
- }}
- path={path}
- onLogout={clearTokenAndGoToRoot}
- setInstanceName={setInstanceName}
- isPasswordOk={defaultToken !== undefined}
- />
- <KycBanner />
- <NotificationCard notification={globalNotification} />
-
- <Router
- onChange={(e) => {
- const movingOutFromNotification =
- globalNotification && e.url !== globalNotification.to;
- if (movingOutFromNotification) {
- setGlobalNotification(undefined);
- }
- }}
- >
- {/**
- * 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={InstancePaths.settings}
- component={InstanceUpdatePage}
- onBack={() => {
- route(`/`);
- }}
- onConfirm={() => {
- route(`/`);
- }}
- onUpdateError={noop}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
- />
- {/**
- * Inventory pages
- */}
- <Route
- path={InstancePaths.inventory_list}
- component={ProductListPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
- onCreate={() => {
- route(InstancePaths.inventory_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.inventory_update.replace(":pid", id));
- }}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- />
- <Route
- path={InstancePaths.inventory_update}
- component={ProductUpdatePage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
- onConfirm={() => {
- route(InstancePaths.inventory_list);
- }}
- onBack={() => {
- route(InstancePaths.inventory_list);
- }}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- />
- <Route
- path={InstancePaths.inventory_new}
- component={ProductCreatePage}
- onConfirm={() => {
- route(InstancePaths.inventory_list);
- }}
- onBack={() => {
- route(InstancePaths.inventory_list);
- }}
- />
- {/**
- * Deposit confirmation pages
- */}
- <Route
- path={InstancePaths.deposit_confirmation_list}
- component={DepositConfirmationListPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
- onCreate={() => {
- route(InstancePaths.deposit_confirmation_new);
- }}
- onSelect={(id: string) => {
- route(InstancePaths.deposit_confirmation_update.replace(":pid", id));
- }}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- />
- <Route
- path={InstancePaths.deposit_confirmation_update}
- component={DepositConfirmationUpdatePage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)}
- onConfirm={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
- onBack={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- />
- <Route
- path={InstancePaths.deposit_confirmation_new}
- component={DepositConfirmationCreatePage}
- onConfirm={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
- onBack={() => {
- route(InstancePaths.deposit_confirmation_list);
- }}
- />
- <Route path={InstancePaths.interface} component={Settings} />
- {/**
- * Example pages
- */}
- <Route path="/loading" component={Loading} />
- <Route default component={NotFoundPage} />
- </Router>
- </InstanceContextProvider>
- );
}
-export function Redirect({ to }: { to: string }): null {
- useEffect(() => {
- route(to, true);
- });
- return null;
+export interface Props {
+ path: string;
}
-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",
+export function InstanceRoutes({
+ // id,
+ path,
+ // setInstanceName
+ }: Props): VNode {
+ const {i18n} = useTranslationContext();
+
+ type GlobalNotifState = (Notification & { to: string | undefined }) | undefined;
+ const [globalNotification, setGlobalNotification] =
+ useState<GlobalNotifState>(undefined);
+
+ const [error] = useErrorBoundary();
+ const {title, endpoint, entity} = getInstanceTitle(path.replace("app/#", ""));
+
+ const value = useMemo(
+ () => ({title, path, endpoint, entity}),
+ [title, path, endpoint, entity],
+ );
+
+ //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 (
+ <EntityContextProvider value={value}>
+ <Menu
+ // instance={id}
+ path={path}
+ title={"Settings"}
+ onShowSettings={() => {
+ route(Paths.settings);
+ }}/>
+ <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 =
+ globalNotification && e.url !== globalNotification.to;
+ if (movingOutFromNotification) {
+ setGlobalNotification(undefined);
+ }
}}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- );
- }}
- />
- </InstanceContextProvider>
- );
-}
+ >
+ <Route path="/" component={Redirect} to={Paths.key_figures}/>
-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>
- ),
- }}
- />
- );
+ <Route
+ path={Paths.key_figures}
+ component={FinanceDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.critical_errors}
+ component={SecurityDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.operating_status}
+ component={OperationsDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.detail_view}
+ component={DetailsDashboard}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.amount_arithmethic_inconsistency_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ path={Paths.bad_sig_losses_list}
+ component={DefaultList}
+ onNotFound={NotFoundPage}
+ //onLoadError={ServerErrorRedirectTo(Paths.balance_list)}
+ />
+ <Route
+ 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={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>
+ </EntityContextProvider>
+ );
}
+
+export function Redirect({to}: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+} \ No newline at end of file
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..11b62c124 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
@@ -22,27 +22,27 @@
import { h, VNode } from "preact";
export function Loading(): VNode {
- return (
- <div
- class="columns is-centered is-vcentered"
- style={{
- height: "calc(100% - 3rem)",
- position: "absolute",
- width: "100%",
- }}
- >
- <Spinner />
- </div>
- );
+ return (
+ <div
+ class="columns is-centered is-vcentered"
+ style={{
+ height: "calc(100% - 3rem)",
+ position: "absolute",
+ width: "100%",
+ }}
+ >
+ <Spinner />
+ </div>
+ );
}
export function Spinner(): VNode {
- return (
- <div class="lds-ring">
- <div />
- <div />
- <div />
- <div />
- </div>
- );
+ return (
+ <div class="lds-ring">
+ <div />
+ <div />
+ <div />
+ <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/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/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/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/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/FormProvider.tsx b/packages/auditor-backoffice-ui/src/components/forms/FormProvider.tsx
index 0d53c4d08..a5f3c1d2f 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
diff --git a/packages/auditor-backoffice-ui/src/components/form/Input.tsx b/packages/auditor-backoffice-ui/src/components/forms/Input.tsx
index c1ddcb064..899061c35 100644
--- a/packages/auditor-backoffice-ui/src/components/form/Input.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/Input.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/InputCurrency.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputCurrency.tsx
index b02354d7c..c1359e641 100644
--- a/packages/auditor-backoffice-ui/src/components/form/InputCurrency.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/InputCurrency.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/InputNumber.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputNumber.tsx
index 3b5df1474..10b28cd93 100644
--- a/packages/auditor-backoffice-ui/src/components/form/InputNumber.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/InputNumber.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/InputSelector.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputSelector.tsx
index a8dad5d89..f567f7247 100644
--- a/packages/auditor-backoffice-ui/src/components/form/InputSelector.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/InputSelector.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/InputToggle.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputToggle.tsx
index f95dfcd05..89b815b4b 100644
--- a/packages/auditor-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/InputToggle.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/InputWithAddon.tsx b/packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.tsx
index e9fd88770..b8cd4c2d2 100644
--- a/packages/auditor-backoffice-ui/src/components/form/InputWithAddon.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/InputWithAddon.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/useField.tsx b/packages/auditor-backoffice-ui/src/components/forms/useField.tsx
index c7559faae..49bba4984 100644
--- a/packages/auditor-backoffice-ui/src/components/form/useField.tsx
+++ b/packages/auditor-backoffice-ui/src/components/forms/useField.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/index.stories.ts b/packages/auditor-backoffice-ui/src/components/index.stories.ts
deleted file mode 100644
index c57ddab14..000000000
--- a/packages/auditor-backoffice-ui/src/components/index.stories.ts
+++ /dev/null
@@ -1,17 +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/>
- */
-
-export * as payto from "./form/InputPaytoForm.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/auditor-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
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..d81410bdf 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
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..e411939c7 100644
--- a/packages/auditor-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/auditor-backoffice-ui/src/components/menu/index.tsx
@@ -1,237 +1,242 @@
/*
- This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
- GNU Taler is free software; you can redistribute it and/or modify it under the
- 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.
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { AdminPaths } from "../../AdminRoutes.js";
-import { InstancePaths } from "../../InstanceRoutes.js";
-import { Notification } from "../../utils/types.js";
-import { NavigationBar } from "./NavigationBar.js";
-import { Sidebar } from "./SideBar.js";
-
-function getInstanceTitle(path: string, id: string): string {
- switch (path) {
- case InstancePaths.settings:
- return `${id}: Settings`;
- case InstancePaths.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`;
- default:
- return "";
- }
-}
+You should have received a copy of the GNU General Public License along with
+GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+*/
-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);
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
+ */
+
+import {ComponentChildren, Fragment, h, VNode} from "preact";
+import {useEffect, useState} from "preact/hooks";
+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): string {
+ switch (path) {
+ 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 "";
+ }
}
interface MenuProps {
- title?: string;
- path: string;
- instance: string;
- admin?: boolean;
- onLogout?: () => void;
- onShowSettings: () => void;
- setInstanceName: (s: string) => void;
- isPasswordOk: boolean;
+ title?: string;
+ path: string;
+ onShowSettings: () => void;
}
function WithTitle({
- title,
- children,
-}: {
- title: string;
- children: ComponentChildren;
+ title,
+ children,
+ }: {
+ title: string;
+ children: ComponentChildren;
}): VNode {
- useEffect(() => {
- document.title = `Taler Backoffice: ${title}`;
- }, [title]);
- return <Fragment>{children}</Fragment>;
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
}
export function Menu({
- onLogout,
- onShowSettings,
- title,
- instance,
- path,
- admin,
- setInstanceName,
- isPasswordOk
-}: MenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false);
-
- const titleWithSubtitle = title
- ? title
- : !admin
- ? getInstanceTitle(path, instance)
- : getAdminTitle(path, instance);
- const adminInstance = instance === "default";
- const mimic = admin && !adminInstance;
- return (
- <WithTitle title={titleWithSubtitle}>
- <div
- class={mobileOpen ? "has-aside-mobile-expanded" : ""}
- onClick={() => setMobileOpen(false)}
- >
- <NavigationBar
- onMobileMenu={() => setMobileOpen(!mobileOpen)}
- title={titleWithSubtitle}
- />
-
- {onLogout && (
- <Sidebar
- onShowSettings={onShowSettings}
- onLogout={onLogout}
- admin={admin}
- mimic={mimic}
- instance={instance}
- mobile={mobileOpen}
- isPasswordOk={isPasswordOk}
- />
- )}
-
- {mimic && (
- <nav class="level" style={{
- zIndex: 100,
- position: "fixed",
- width: "50%",
- marginLeft: "20%"
- }}>
- <div class="level-item has-text-centered has-background-warning">
- <p class="is-size-5">
- You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
- <a
- href="#/instances"
- onClick={(e) => {
- setInstanceName("default");
- }}
- >
- go back
- </a>
- </p>
+ onShowSettings,
+ title,
+ path,
+ }: MenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const titleWithSubtitle = getInstanceTitle(path.replace("app/#", ""));
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ <Sidebar
+ onShowSettings={onShowSettings}
+ mobile={mobileOpen}
+ />
</div>
- </nav>
- )}
- </div>
- </WithTitle>
- );
+ </WithTitle>
+ );
}
interface NotYetReadyAppMenuProps {
- title: string;
- onShowSettings: () => void;
- onLogout?: () => void;
- isPasswordOk: boolean;
+ title: string;
+ onShowSettings: () => void;
}
interface NotifProps {
- notification?: Notification;
+ notification?: Notification;
}
-export function NotificationCard({
- notification: n,
-}: NotifProps): VNode | null {
- if (!n) return null;
- return (
- <div class="notification">
- <div class="columns is-vcentered">
- <div class="column is-12">
- <article
- class={
- n.type === "ERROR"
- ? "message is-danger"
- : n.type === "WARN"
- ? "message is-warning"
- : "message is-info"
- }
- >
- <div class="message-header">
- <p>{n.message}</p>
+
+export function NotificationCard({notification: n}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && (
+ <div class="message-body">
+ <div>{n.description}</div>
+ {n.details && <pre>{n.details}</pre>}
+ </div>
+ )}
+ </article>
+ </div>
</div>
- {n.description && (
- <div class="message-body">
- <div>{n.description}</div>
- {n.details && <pre>{n.details}</pre>}
- </div>
- )}
- </article>
</div>
- </div>
- </div>
- );
+ );
}
interface NotConnectedAppMenuProps {
- title: string;
+ title: string;
}
+
export function NotConnectedAppMenu({
- title,
-}: NotConnectedAppMenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false);
-
- useEffect(() => {
- document.title = `Taler Backoffice: ${title}`;
- }, [title]);
-
- return (
- <div
- class={mobileOpen ? "has-aside-mobile-expanded" : ""}
- onClick={() => setMobileOpen(false)}
- >
- <NavigationBar
- onMobileMenu={() => setMobileOpen(!mobileOpen)}
- title={title}
- />
- </div>
- );
+ title,
+ }: NotConnectedAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ </div>
+ );
}
+
export function NotYetReadyAppMenu({
- onLogout,
- onShowSettings,
- title,
- isPasswordOk
-}: NotYetReadyAppMenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false);
-
- useEffect(() => {
- document.title = `Taler Backoffice: ${title}`;
- }, [title]);
-
- return (
- <div
- class={mobileOpen ? "has-aside-mobile-expanded" : ""}
- onClick={() => setMobileOpen(false)}
- >
- <NavigationBar
- onMobileMenu={() => setMobileOpen(!mobileOpen)}
- title={title}
- />
- {onLogout && (
- <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
- )}
- </div>
- );
-}
+ onShowSettings,
+ title
+ }: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ (
+ <Sidebar onShowSettings={onShowSettings} instance="" mobile={mobileOpen}/>
+ )
+ </div>
+ );
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/components/modal/index.tsx b/packages/auditor-backoffice-ui/src/components/modal/index.tsx
index 8372c84cc..ab2834d86 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
@@ -22,11 +22,11 @@
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 { useEntityContext } from "../../context/entity.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";
+import { FormProvider } from "../forms/FormProvider.js";
+import { Input } from "../forms/Input.js";
interface Props {
active?: boolean;
@@ -310,9 +310,9 @@ export function UpdateTokenModal({
(k) => (errors as any)[k] !== undefined,
);
- const instance = useInstanceContext();
+ const instance = useEntityContext();
- const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
+ const text = i18n.str`You are updating the access token from instance with id `;
return (
<ClearConfirmModal
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..ce321c3e6 100644
--- a/packages/auditor-backoffice-ui/src/context/backend.ts
+++ b/packages/auditor-backoffice-ui/src/context/backend.ts
@@ -17,54 +17,54 @@
/**
*
* @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,
+ url: "",
});
function useBackendContextState(
- defaultUrl?: string,
+ defaultUrl?: string,
): BackendContextType {
-const [url] = useBackendURL(defaultUrl);
- //const url = "http://localhost:8081";
- const [token, updateToken] = useBackendDefaultToken();
-
- return {
- url,
- token,
- alreadyTriedLogin: token !== undefined,
- updateToken,
- };
+ const [url] = useBackendURL(defaultUrl);
+
+ return {
+ url,
+ };
}
export const BackendContextProvider = ({
- children,
- defaultUrl,
-}: {
- children: any;
- defaultUrl?: string;
+ children,
+ defaultUrl,
+ }: {
+ children: any;
+ defaultUrl?: string;
}): VNode => {
- const value = useBackendContextState(defaultUrl);
+ const value = useBackendContextState(defaultUrl);
- return h(BackendContext.Provider, { value, children });
+ return h(BackendContext.Provider, { value, children });
};
+
+
export const useBackendContext = (): BackendContextType =>
- useContext(BackendContext);
+ useContext(BackendContext);
+
+interface BackendTokenType {
+ token: string;
+}
+
+const BackendTokenContext = createContext<BackendTokenType>({} as any);
+
+export const BackendTokenContextProvider = BackendTokenContext.Provider;
+
+export const useBackendTokenContext = (): BackendTokenType => useContext(BackendTokenContext); \ No newline at end of file
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..a8cdee53c 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,6 +17,7 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
type HashCode = string;
@@ -25,7 +26,7 @@ type EddsaSignature = string;
type WireTransferIdentifierRawP = string;
type RelativeTime = TalerProtocolDuration;
type ImageDataUrl = string;
-type MerchantUserType = "business" | "individual";
+type AuditorUserType = "business" | "individual";
export interface WithId {
@@ -38,9 +39,11 @@ interface Timestamp {
// never happen.
t_s: number | "never";
}
+
interface TalerProtocolDuration {
d_us: number | "forever";
}
+
interface Duration {
d_ms: number | "forever";
}
@@ -53,263 +56,51 @@ type Amount = string;
type UUID = string;
type Integer = number;
-interface WireAccount {
- // payto:// URI identifying the account and wire method
- payto_uri: string;
-
- // URI to convert amounts from or to the currency used by
- // this wire account of the exchange. Missing if no
- // conversion is applicable.
- conversion_url?: string;
-
- // Restrictions that apply to bank accounts that would send
- // funds to the exchange (crediting this exchange bank account).
- // Optional, empty array for unrestricted.
- credit_restrictions: AccountRestriction[];
-
- // Restrictions that apply to bank accounts that would receive
- // funds from the exchange (debiting this exchange bank account).
- // Optional, empty array for unrestricted.
- debit_restrictions: AccountRestriction[];
-
- // Signature using the exchange's offline key over
- // a TALER_MasterWireDetailsPS
- // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
- master_sig: EddsaSignature;
-}
-
-type AccountRestriction = RegexAccountRestriction | DenyAllAccountRestriction;
-
-// Account restriction that disables this type of
-// account for the indicated operation categorically.
-interface DenyAllAccountRestriction {
- type: "deny";
-}
-
-// Accounts interacting with this type of account
-// restriction must have a payto://-URI matching
-// the given regex.
-interface RegexAccountRestriction {
- type: "regex";
-
- // Regular expression that the payto://-URI of the
- // partner account must follow. The regular expression
- // should follow posix-egrep, but without support for character
- // classes, GNU extensions, back-references or intervals. See
- // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
- // for a description of the posix-egrep syntax. Applications
- // may support regexes with additional features, but exchanges
- // must not use such regexes.
- payto_regex: string;
-
- // Hint for a human to understand the restriction
- // (that is hopefully easier to comprehend than the regex itself).
- human_hint: string;
-
- // Map from IETF BCP 47 language tags to localized
- // human hints.
- human_hint_i18n?: { [lang_tag: string]: string };
-}
-interface LoginToken {
- token: string,
- expiration: Timestamp,
-}
-// token used to get loginToken
-// must forget after used
-declare const __ac_token: unique symbol;
-type AccessToken = string & {
- [__ac_token]: true;
-};
-
-export namespace ExchangeBackend {
- interface WireResponse {
- // Master public key of the exchange, must match the key returned in /keys.
- master_public_key: EddsaPublicKey;
-
- // Array of wire accounts operated by the exchange for
- // incoming wire transfers.
- accounts: WireAccount[];
-
- // Object mapping names of wire methods (i.e. "sepa" or "x-taler-bank")
- // to wire fees.
- fees: { method: AggregateTransferFee };
- }
- interface AggregateTransferFee {
- // Per transfer wire transfer fee.
- wire_fee: Amount;
-
- // Per transfer closing fee.
- closing_fee: Amount;
-
- // What date (inclusive) does this fee go into effect?
- // The different fees must cover the full time period in which
- // any of the denomination keys are valid without overlap.
- start_date: Timestamp;
-
- // What date (exclusive) does this fee stop going into effect?
- // The different fees must cover the full time period in which
- // any of the denomination keys are valid without overlap.
- end_date: Timestamp;
-
- // Signature of TALER_MasterWireFeePS with
- // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
- sig: EddsaSignature;
- }
-}
export namespace AuditorBackend {
- interface ErrorDetail {
- // Numeric error code unique to the condition.
- // The other arguments are specific to the error value reported here.
- code: number;
+ interface DepositConfirmation {
+ // identifier
+ deposit_confirmation_serial_id: 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;
+ h_contract_terms: string;
- // Optional detail about the specific input value that failed. May change without notice!
- detail?: string;
+ h_policy: string;
- // Name of the parameter that was bogus (if applicable).
- parameter?: string;
+ h_wire: string;
- // Path to the argument that was bogus (if applicable).
- path?: string;
+ exchange_timestamp: string;
- // Offset of the argument that was bogus (if applicable).
- offset?: string;
+ refund_deadline: string;
- // Index of the argument that was bogus (if applicable).
- index?: string;
+ wire_deadline: string;
- // Name of the object that was bogus (if applicable).
- object?: string;
+ total_without_fee: string;
- // Name of the currency than was problematic (if applicable).
- currency?: string;
+ coin_pubs: string;
- // Expected type (if applicable).
- type_expected?: string;
+ coin_sigs: string;
- // Type that was provided instead (if applicable).
- type_actual?: string;
- }
- interface Exchange {
- // the exchange's base URL
- url: string;
-
- // master public key of the exchange
- master_pub: EddsaPublicKey;
- }
- namespace DepositConfirmation {
- // POST /deposit-confirmation
- interface ProductAddDetail {
- // product ID to use.
- product_id: string;
-
- // Human-readable product description.
- description: string;
+ merchant_pub: string;
- // Map from IETF BCP 47 language tags to localized descriptions
- description_i18n: { [lang_tag: string]: string };
+ merchant_sig: string;
- // unit in which the product is measured (liters, kilograms, packages, etc.)
- unit: string;
+ exchange_pub: 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;
+ exchange_sig: string;
- // An optional base64-encoded product image
- image: ImageDataUrl;
+ suppressed: string;
- // 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;
- }
+ ancient: string;
+ }
- // GET /deposit-confirmation/$SERIAL_ID
- interface DepositConfirmationDetail {
- serial_id: string;
- timestamp: string;
- refund_deadline: string;
- wire_deadline: string;
- amount_without_fee: string;
- }
+ interface Config {
+ name: string;
+ version: string;
+ implementation: string;
+ currency: string;
+ auditor_public_key: string;
+ exchange_master_public_key: string;
}
-}
-export namespace MerchantBackend {
interface ErrorDetail {
// Numeric error code unique to the condition.
// The other arguments are specific to the error value reported here.
@@ -347,1447 +138,618 @@ export namespace MerchantBackend {
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.
+ name: "taler-auditor";
+ version: 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.
+ implementation: string;
currency: string;
+ auditor_public_key: string;
+ exchange_master_public_key: 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: CurrencySpecification };
+
+ // Array of exchanges trusted by the merchant.
+ // Since protocol v6.
+ // exchanges: ExchangeConfigInfo[];
}
- 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 TokenResponse {
+ null;
}
- 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;
+ namespace Default {
+ interface ObjectResponse {
+ object: AnyEntry[];
}
+ }
- // 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;
+ namespace AmountArithmeticInconsistency {
- // If the frontend does NOT specify a payment deadline, how long should
- // offers we make be valid by default?
- default_pay_delay: RelativeTime;
+ class ClassAmountArithmeticInconsistency {
+ data: AmountArithmeticInconsistencyDetail[];
}
- // GET /private/instances
- interface InstancesResponse {
- // List of instances that are present in the backend (see Instance)
- instances: Instance[];
+ interface SummaryResponse {
+ amount_arithmetic_inconsistency: AmountArithmeticInconsistencyDetail[];
}
- 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;
+ interface AmountArithmeticInconsistencyDetail {
+ row_id: number;
+ operation: string;
+ exchange_amount: string;
+ auditor_amount: string;
+ profitable: boolean;
+ suppressed: 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";
- };
+ namespace BadSigLoss {
+ class ClassBadSigLoss {
+ data: BadSigLossDetail[];
}
- // 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 SummaryResponse {
+ amount_arithmetic_inconsistency: BadSigLossDetail[];
}
- 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;
+ interface BadSigLossDetail
+ {
+ row_id: number;
+ operation: string;
+ loss: string;
+ operation_specific_pub: string;
+ suppressed: boolean;
}
}
- namespace KYC {
- //GET /private/instances/$INSTANCE/kyc
- interface AccountKycRedirects {
- // Array of pending KYCs.
- pending_kycs: MerchantAccountKycRedirect[];
+ namespace Balance {
- // Array of exchanges with no reply.
- timeout_kycs: ExchangeKycTimeout[];
+ class ClassBalance {
+ // List of products that are present in the inventory
+ data: BalanceDetail[];
}
- 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;
+ interface SummaryResponse {
+ // List of products that are present in the inventory
+ balances: BalanceDetail[];
+ }
- // AML status of the account.
- aml_status: number;
+ interface BalanceDetail {
+ // identifier
+ row_id: number;
- // Our bank wire account this is about.
- payto_uri: string;
- }
- interface ExchangeKycTimeout {
- // Base URL of the exchange this is about.
- exchange_url: string;
+ balance_key: string;
- // Numeric error code indicating errors the exchange
- // returned, or TALER_EC_INVALID for none.
- exchange_code: number;
+ balance_value: string;
- // 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;
+ suppressed: boolean;
}
-
}
- 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;
-
+ namespace ClosureLag {
+ class ClassClosureLag {
+ // List of products that are present in the inventory
+ data: ClosureLagDetail[];
}
- type FacadeCredentials =
- | NoFacadeCredentials
- | BasicAuthFacadeCredentials;
-
- interface NoFacadeCredentials {
- type: "none";
+ interface SummaryResponse {
+ // List of products that are present in the inventory
+ closure_lags: ClosureLagDetail[];
}
- interface BasicAuthFacadeCredentials {
- type: "basic";
-
- // Username to use to authenticate
- username: string;
-
- // Password to use to authenticate
- password: string;
+ interface ClosureLagDetail {
+ row_id: number;
+ amount: string;
+ deadline: number;
+ wtid: number;
+ account: string;
+ suppressed: boolean;
}
+ }
- interface AccountAddResponse {
- // Hash over the wire details (including over the salt).
- h_wire: HashCode;
-
- // Salt used to compute h_wire.
- salt: HashCode;
+ namespace CoinInconsistency {
+ class ClassCoinInconsistency {
+ data: CoinInconsistencyDetail[];
}
- 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 SummaryResponse {
+ amount_arithmetic_inconsistency: CoinInconsistencyDetail[];
}
-
- interface AccountsSummaryResponse {
-
- // List of accounts that are known for the instance.
- accounts: BankAccountEntry[];
+ interface CoinInconsistencyDetail
+ {
+ row_id: number;
+ operation: string;
+ exchange_amount: string;
+ auditor_amount: string;
+ coin_pub: string;
+ profitable: boolean;
+ suppressed: boolean;
}
+ }
- 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;
+ namespace DenominationKeyValidityWithdrawInconsistency {
+ class ClassDenominationKeyValidityWithdrawInconsistency {
+ data: DenominationKeyValidityWithdrawInconsistencyDetail[];
+ }
- // true if this account is active,
- // false if it is historic.
- active: boolean;
+ interface SummaryResponse {
+ responseData: DenominationKeyValidityWithdrawInconsistencyDetail[];
}
+ interface DenominationKeyValidityWithdrawInconsistencyDetail
+ {
+ row_id: number;
+ operation: string;
+ loss: string;
+ operation_specific_pub: string;
+ suppressed: 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;
+ namespace DenominationPending {
+ class ClassDenominationPending {
+ data: DenominationPendingDetail[];
}
- // 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;
+ interface SummaryResponse {
+ responseData: DenominationPendingDetail[];
}
- // GET /private/products
- interface InventorySummaryResponse {
- // List of products that are present in the inventory
- products: InventoryEntry[];
+ interface DenominationPendingDetail
+ {
+ denom_pub_hash: string;
+ denom_balance: string;
+ denom_loss: string;
+ num_issued: number;
+ denom_risk: string;
+ recoup_loss: string;
+ suppressed: boolean;
}
- 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;
+ namespace DenominationsWithoutSigs {
+ class ClassDenominationsWithoutSigs {
+ data: DenominationsWithoutSigsDetail[];
}
- // POST /private/products/$PRODUCT_ID/lock
- interface LockRequest {
- // UUID that identifies the frontend performing the lock
- // It is suggested that clients use a timeflake for this,
- // see https://github.com/anthonynsimon/timeflake
- lock_uuid: UUID;
-
- // How long does the frontend intend to hold the lock
- duration: RelativeTime;
-
- // How many units should be locked?
- quantity: Integer;
+ interface SummaryResponse {
+ responseData: DenominationsWithoutSigsDetail[];
}
- // DELETE /private/products/$PRODUCT_ID
+ interface DenominationsWithoutSigsDetail
+ {
+ row_id: number;
+ denompub_h: string;
+ value: string;
+ start_time: number;
+ end_time: number;
+ suppressed: boolean;
+ }
}
- 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;
+ namespace DepositConfirmation {
+ class ClassDepositConfirmation{
+ data: DepositConfirmationDetail[];
}
- interface CheckPaymentClaimedResponse {
- // A wallet claimed the order, but did not yet pay for the contract.
- order_status: "claimed";
- // Contract terms.
- contract_terms: ContractTerms;
+ interface SummaryResponse {
+ responseData: DepositConfirmationDetail[];
}
- 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 DepositConfirmationDetail {
+ deposit_confirmation_serial_id: number;
+ h_contract_terms: string;
+ h_policy: string;
+ h_wire: string;
+ exchange_timestamp: string;
+ refund_deadline: string;
+ wire_deadline: string;
+ total_without_fee: string;
+ coin_pubs: string;
+ coin_sigs: string;
+ merchant_pub: string;
+ merchant_sig: string;
+ exchange_pub: string;
+ exchange_sig: string;
+ suppressed: string;
+ ancient: string;
}
- 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 Emergency {
+ class ClassEmergency{
+ data: EmergencyDetail[];
}
- 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 {
+ responseData: EmergencyDetail[];
}
- 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;
+ interface EmergencyDetail
+ {
+ row_id: number;
+ denompub_h: string;
+ denom_risk: string;
+ denom_loss: string;
+ deposit_start: number;
+ deposit_end: number;
+ value: string;
+ }
+ }
- // Public key of the coin for which we got the exchange error.
- coin_pub: CoinPublicKey;
+ namespace EmergencyByCount {
+ class ClassEmergencyByCount{
+ data: EmergencyByCountDetail[];
}
- 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 SummaryResponse {
+ responseData: EmergencyByCountDetail[];
}
- interface OrderHistoryEntry {
- // order ID of the transaction related to this entry.
- order_id: string;
- // row ID of the order in the database
+ interface EmergencyByCountDetail
+ {
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;
+ denompub_h: string;
+ num_issued: number;
+ num_known: number;
+ risk: string;
+ start: number;
+ deposit_end: number;
+ value: string;
+ suppressed: 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;
+ namespace FeeTimeInconsistency {
+ class ClassFeeTimeInconsistency{
+ data: FeeTimeInconsistencyDetail[];
}
- 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 SummaryResponse {
+ responseData: FeeTimeInconsistencyDetail[];
}
- interface MinimalInventoryProduct {
- // Which product is requested (here mandatory!)
- product_id: string;
-
- // How many units of the product are requested
- quantity: Integer;
+ interface FeeTimeInconsistencyDetail
+ {
+ row_id: number;
+ type: string;
+ time: string;
+ diagnostic: string;
+ suppressed: boolean;
}
- 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;
+ namespace HistoricDenominationRevenue {
+ class ClassHistoricDenominationRevenue {
+ data: HistoricDenominationRevenueDetail[];
}
- 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 SummaryResponse {
+ responseData: HistoricDenominationRevenueDetail[];
}
- 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;
+ interface HistoricDenominationRevenueDetail
+ {
+ denom_pub_hash: string;
+ revenue_timestamp: number;
+ revenue_balance: string;
+ loss_balance: string;
+ suppressed: boolean;
}
}
- namespace Rewards {
- // GET /private/reserves
- interface RewardReserveStatus {
- // Array of all known reserves (possibly empty!)
- reserves: ReserveStatusEntry[];
+ namespace HistoricReserveSummary {
+ class ClassHistoricReserveSummary {
+ data: HistoricReserveSummaryDetail[];
}
- 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 SummaryResponse {
+ responseData: HistoricReserveSummaryDetail[];
}
- 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 HistoricReserveSummaryDetail
+ {
+ denom_pub_hash: string;
+ revenue_timestamp: number;
+ revenue_balance: string;
+ loss_balance: string;
+ suppressed: boolean;
}
- interface ReserveCreateConfirmation {
- // Public key identifying the reserve
- reserve_pub: EddsaPublicKey;
+ }
- // Wire accounts of the exchange where to transfer the funds.
- accounts: WireAccount[];
+ namespace MisattributionInInconsistency {
+ class ClassMisattributionInInconsistency {
+ data: MisattributionInInconsistencyDetail[];
}
- 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 SummaryResponse {
+ responseData: MisattributionInInconsistencyDetail[];
}
- 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 MisattributionInInconsistencyDetail
+ {
+ row_id: number;
+ amount: string;
+ bank_row: number;
+ reserve_pub: string;
+ suppressed: boolean;
}
+ }
- 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;
+ namespace Progress {
+ class ClassProgress {
+ data: ProgressDetail[];
}
- 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 SummaryResponse {
+ responseData: ProgressDetail[];
}
- 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;
+ interface ProgressDetail
+ {
+ progress_key: string;
+ progress_offset: number;
+ suppressed: boolean;
+ }
+ }
- // Array showing the pickup operations of the wallet (possibly empty!).
- // Only present if asked for explicitly.
- pickups?: PickupDetail[];
+ namespace PurseNotClosedInconsistency {
+ class ClassPurseNotClosedInconsistency {
+ data: PurseNotClosedInconsistencyDetail[];
}
- interface PickupDetail {
- // Unique identifier for the pickup operation.
- pickup_id: HashCode;
- // Number of planchets involved.
- num_planchets: Integer;
+ interface SummaryResponse {
+ responseData: PurseNotClosedInconsistencyDetail[];
+ }
- // Total amount requested for this pickup_id.
- requested_amount: Amount;
+ interface PurseNotClosedInconsistencyDetail
+ {
+ row_id: number;
+ purse_pub: string,
+ amount: string;
+ expiration_date: number;
+ suppressed: boolean;
}
}
- namespace Transfers {
- interface TransferList {
- // list of all the transfers that fit the filter that we know
- transfers: TransferDetails[];
+ namespace Purses {
+ class ClassPurses {
+ data: PursesDetail[];
}
- 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 SummaryResponse {
+ responseData: PursesDetail[];
}
- 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;
+ interface PursesDetail
+ {
+ auditor_purses_rowid: number;
+ purse_pub: string;
+ balance: string;
+ target: string,
+ expiration_date: number;
+ 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;
+ namespace RefreshesHanging {
+ class ClassRefreshesHanging {
+ data: RefreshesHangingDetail[];
}
- interface OtpDevicePatchDetails {
- // Human-readable description for the device.
- otp_device_description: string;
-
- // A base64-encoded key
- otp_key: string | undefined;
-
- // Algorithm for computing the POS confirmation.
- otp_algorithm: Integer;
-
- // Counter for counter-based OTP devices.
- otp_ctr?: Integer;
+ interface SummaryResponse {
+ responseData: RefreshesHangingDetail[];
}
- interface OtpDeviceSummaryResponse {
- // Array of devices that are present in our backend.
- otp_devices: OtpDeviceEntry[];
+ interface RefreshesHangingDetail
+ {
+ row_id: number;
+ amount: string;
+ coin_pub: string;
+ suppressed: boolean;
}
- interface OtpDeviceEntry {
- // Device identifier.
- otp_device_id: string;
+ }
- // Human-readable description for the device.
- device_description: string;
+ namespace ReserveBalanceInsufficientInconsistency {
+ class ClassReserveBalanceInsufficientInconsistency {
+ data: ReserveBalanceInsufficientInconsistencyDetail[];
}
- 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;
+ interface SummaryResponse {
+ responseData: ReserveBalanceInsufficientInconsistencyDetail[];
}
-
+ interface ReserveBalanceInsufficientInconsistencyDetail
+ {
+ row_id: number;
+ reserve_pub: string;
+ inconsistency_gain: boolean;
+ inconsistency_amount: string;
+ suppressed: boolean;
+ }
}
- 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;
+ namespace ReserveBalanceSummaryWrongInconsistency {
+ class ClassReserveBalanceSummaryWrongInconsistency {
+ data: ReserveBalanceSummaryWrongInconsistencyDetail[];
}
- 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 SummaryResponse {
+ responseData: ReserveBalanceSummaryWrongInconsistencyDetail[];
}
- 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 ReserveBalanceSummaryWrongInconsistencyDetail
+ {
+ row_id: number;
+ reserve_pub: string;
+ exchange_amount: string;
+ auditor_amount: string;
+ suppressed: boolean;
}
+ }
- interface TemplateSummaryResponse {
- // List of templates that are present in our backend.
- templates: TemplateEntry[];
+ namespace ReserveInInconsistency {
+ class ClassReserveInInconsistency {
+ data: ReserveInInconsistencyDetail[];
}
- interface TemplateEntry {
- // Template identifier, as found in the template.
- template_id: string;
-
- // Human-readable description for the template.
- template_description: string;
+ interface SummaryResponse {
+ responseData: ReserveInInconsistencyDetail[];
}
- 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 ReserveInInconsistencyDetail
+ {
+ row_id: number;
+ amount_exchange_expected: string;
+ amount_wired: string;
+ reserve_pub: string;
+ timestamp: number;
+ account: string;
+ diagnostic: string;
+ suppressed: boolean;
}
+ }
- interface UsingTemplateDetails {
- // Subject of the template
- summary?: string;
+ namespace ReserveNotClosedInconsistency {
+ class ClassReserveNotClosedInconsistency {
+ data: ReserveNotClosedInconsistencyDetail[];
+ }
- // The amount entered by the customer.
- amount?: Amount;
+ interface SummaryResponse {
+ responseData: ReserveNotClosedInconsistencyDetail[];
}
- interface UsingTemplateResponse {
- // After enter the request. The user will be pay with a taler URL.
- order_id: string;
- token: string;
+ interface ReserveNotClosedInconsistencyDetail
+ {
+ row_id: number;
+ reserve_pub: string;
+ balance: string;
+ expiration_time: number;
+ diagnostic: string;
+ suppressed: boolean;
}
}
- 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;
+ namespace Reserves {
+ class ClassReserves{
+ data: ReservesDetail[];
+ }
- // Method used by the webhook
- http_method: string;
+ interface SummaryResponse {
+ responseData: ReservesDetail[];
+ }
- // Header template of the webhook
- header_template?: string;
+ interface ReservesDetail
+ {
+ auditor_reserves_rowid: number;
+ reserve_pub: string;
+ reserve_balance: string;
+ reserve_loss: string;
+ withdraw_fee_balance: string;
+ close_fee_balance: string;
+ purse_fee_balance: string;
+ open_fee_balance: string;
+ history_fee_balance: string;
+ expiration_date: number;
+ origin_account: string;
+ suppressed: boolean;
+ }
+ }
- // Body template by the webhook
- body_template?: string;
+ namespace RowInconsistency {
+ class ClassRowInconsistency {
+ data: RowInconsistencyDetail[];
}
- 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;
+ interface SummaryResponse {
+ responseData: RowInconsistencyDetail[];
+ }
- // Method used by the webhook
- http_method: string;
+ interface RowInconsistencyDetail
+ {
+ row_id: number;
+ row_table: string;
+ diagnostic: string;
+ suppressed: boolean;
+ }
+ }
- // Header template of the webhook
- header_template?: string;
+ namespace RowMinorInconsistency {
+ class ClassRowMinorInconsistency {
+ data: RowMinorInconsistencyDetail[];
+ }
- // Body template by the webhook
- body_template?: string;
+ interface SummaryResponse {
+ responseData: RowMinorInconsistencyDetail[];
}
- interface WebhookSummaryResponse {
- // List of webhooks that are present in our backend.
- webhooks: WebhookEntry[];
+
+ interface RowMinorInconsistencyDetail
+ {
+ row_id: number;
+ row_table: string;
+ diagnostic: string;
+ suppressed: boolean;
}
- 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;
+ namespace WireFormatInconsistency {
+ class ClassWireFormatInconsistency {
+ data: WireFormatInconsistencyDetail[];
}
- 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;
+ interface SummaryResponse {
+ responseData: WireFormatInconsistencyDetail[];
+ }
- // Method used by the webhook
- http_method: string;
+ interface WireFormatInconsistencyDetail
+ {
+ row_id: number;
+ amount: string;
+ wire_offset: string;
+ diagnostic: string;
+ suppressed: boolean;
+ }
+ }
- // Header template of the webhook
- header_template?: string;
+ namespace WireOutInconsistency {
+ class ClassWireOutInconsistency{
+ data: WireOutInconsistencyDetail[];
+ }
- // Body template by the webhook
- body_template?: string;
+ interface SummaryResponse {
+ responseData: WireOutInconsistencyDetail[];
}
- }
- 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;
+ interface WireOutInconsistencyDetail
+ {
+ row_id: number;
+ destination_account: string;
+ expected: string;
+ claimed: string;
+ suppressed: boolean;
+ }
}
-}
+} \ No newline at end of file
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..4b0a5a828 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,461 +17,245 @@
/**
*
* @author Sebastian Javier Marchano (sebasjm)
+ * @author Nic Eigel
*/
-import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- RequestError,
- RequestOptions,
- useApiContext,
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+ RequestOptions,
+ useApiContext,
} from "@gnu-taler/web-util/browser";
-import { useCallback, useEffect, useState } from "preact/hooks";
-import { useSWRConfig } from "swr";
+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,
- value?: unknown,
+ re?: RegExp,
+ value?: unknown,
) => Promise<any> {
- const { cache, mutate } = useSWRConfig();
+ const {cache, mutate} = useSWRConfig();
- if (!(cache instanceof Map)) {
- throw new Error(
- "matchMutate requires the cache provider to be a Map instance",
- );
- }
-
- return function matchRegexMutate(re?: RegExp) {
- return mutate((key) => {
- // evict if no key or regex === all
- if (!key || !re) return true
- // match string
- if (typeof key === 'string' && re.test(key)) return true
- // record or object have the path at [0]
- if (typeof key === 'object' && re.test(key[0])) return true
- //key didn't match regex
- return false
- }, undefined, {
- revalidate: true,
- });
- };
+ if (!(cache instanceof Map)) {
+ throw new Error(
+ "matchMutate requires the cache provider to be a Map instance",
+ );
+ }
+
+ return function matchRegexMutate(re?: RegExp) {
+ return mutate((key) => {
+ // evict if no key or regex === all
+ if (!key || !re) return true
+ // match string
+ if (typeof key === 'string' && re.test(key)) return true
+ // record or object have the path at [0]
+ if (typeof key === 'object' && re.test(key[0])) return true
+ //key didn't match regex
+ return false
+ }, undefined, {
+ revalidate: true,
+ });
+ };
}
-export function useBackendInstancesTestForAdmin(): HttpResponse<
- MerchantBackend.Instances.InstancesResponse,
- MerchantBackend.ErrorDetail
+const 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();
+ const {request} = useBackendBaseRequest();
- type Type = MerchantBackend.Instances.InstancesResponse;
+ type Type = AuditorBackend.VersionResponse;
+ type State = { data: HttpResponse<Type, RequestError<AuditorBackend.ErrorDetail>>, timer: number }
+ const [result, setResult] = useState<State>({data: {loading: true}, timer: 0});
- const [result, setResult] = useState<
- HttpResponse<Type, MerchantBackend.ErrorDetail>
- >({ loading: true });
+ useEffect(() => {
+ if (result.timer) {
+ clearTimeout(result.timer);
+ }
- useEffect(() => {
- request<Type>(`/management/instances`)
- .then((data) => setResult(data))
- .catch((error: RequestError<MerchantBackend.ErrorDetail>) =>
- setResult(error.cause),
- );
- }, [request]);
+ 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});
+ });
+ }
- return result;
-}
+ tryConfig();
+ }, [request]);
-const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
-const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+ return result.data;
+}
-export function useBackendConfig(): HttpResponse<
- MerchantBackend.VersionResponse | undefined,
- RequestError<MerchantBackend.ErrorDetail>
+export function useBackendToken(): HttpResponse<
+ AuditorBackend.VersionResponse,
+ RequestError<AuditorBackend.ErrorDetail>
> {
- const { request } = useBackendBaseRequest();
+ 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)
- }
- 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]);
+ useEffect(() => {
+ if (result.timer) {
+ clearTimeout(result.timer);
+ }
+
+ function tryToken(): void {
+ request<Type>(`/monitoring/balances`)
+ .then((data) => {
+ const timer: any = setTimeout(() => {
+ tryToken();
+ }, CHECK_CONFIG_INTERVAL_OK);
+ setResult({data, timer});
+ })
+ .catch((error) => {
+ const timer: any = setTimeout(() => {
+ tryToken();
+ }, CHECK_CONFIG_INTERVAL_FAIL);
+ const data = error.cause;
+ setResult({data, timer});
+ });
+ }
+
+ tryToken();
+ }, [request]);
- return result.data;
+ return result.data;
}
interface useBackendInstanceRequestType {
- request: <T>(
- endpoint: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
- fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
- orderFetcher: <T>(
- params: [endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,]
- ) => Promise<HttpResponseOk<T>>;
- transferFetcher: <T>(
- params: [endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,]
- ) => Promise<HttpResponseOk<T>>;
- templateFetcher: <T>(
- params: [endpoint: string,
- position?: string,
- delta?: number]
- ) => Promise<HttpResponseOk<T>>;
- webhookFetcher: <T>(
- params: [endpoint: string,
- position?: string,
- delta?: number]
- ) => Promise<HttpResponseOk<T>>;
-}
-interface useBackendBaseRequestType {
- request: <T>(
- endpoint: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
-}
-type YesOrNo = "yes" | "no";
-type LoginResult = {
- valid: true;
- token: string;
- expiration: Timestamp;
-} | {
- valid: false;
- cause: HttpError<{}>;
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions,
+ ) => Promise<HttpResponseOk<T>>;
+ fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ multiFetcher: <T>(params: string[]) => Promise<HttpResponseOk<T>[]>;
+ depositConfirmationFetcher: <T>(
+ params: [
+ endpoint: string,
+ ],
+ ) => Promise<HttpResponseOk<T>>;
}
-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: {}
- },
- }
- }
+interface useBackendBaseRequestType {
- return requestNewLoginToken(baseUrl, token.token as AccessToken)
- }
- return { requestNewLoginToken, refreshLoginToken }
+ request: <T>(
+ endpoint: string,
+ options?: RequestOptions
+ ) => Promise<HttpResponseOk<T>>;
}
+type YesOrNo = "yes" | "no";
+
/**
*
* @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 { request: requestHandler } = useApiContext();
- const token = loginToken?.token;
-
- const request = useCallback(
- function requestImpl<T>(
- endpoint: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
- return res
- }).catch(err => {
- throw err
- });
- },
- [backend, token],
- );
-
- return { request };
+ const {url: backend} = useBackendContext();
+ const {request: requestHandler} = useApiContext();
+ //const { token } = useBackendTokenContext();
+ const token = "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0";
+
+
+ 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;
+ });
+ },
+ [backend],
+ );
+
+ return {request};
}
-export function useBackendInstanceRequest(): useBackendInstanceRequestType {
- const { url: rootBackendUrl, token: rootToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
- const { request: requestHandler } = useApiContext();
-
- const { baseUrl, token: loginToken } = !admin
- ? { baseUrl: rootBackendUrl, token: rootToken }
- : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
-
- const token = loginToken?.token;
-
- const request = useCallback(
- function requestImpl<T>(
- endpoint: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { token, ...options });
- },
- [baseUrl, token],
- );
-
- const multiFetcher = useCallback(
- function multiFetcherImpl<T>(
- args: [endpoints: string[]],
- ): Promise<HttpResponseOk<T>[]> {
- const [endpoints] = args
- return Promise.all(
- endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { token }),
- ),
- );
- },
- [baseUrl, token],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { token });
- },
- [baseUrl, token],
- );
-
- const orderFetcher = useCallback(
- function orderFetcherImpl<T>(
- args: [endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,]
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, paid, refunded, wired, searchDate, delta] = args
- const date_s =
- delta && delta < 0 && searchDate
- ? Math.floor(searchDate.getTime() / 1000) + 1
- : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined;
- const params: any = {};
- if (paid !== undefined) params.paid = paid;
- if (delta !== undefined) params.delta = delta;
- if (refunded !== undefined) params.refunded = refunded;
- if (wired !== undefined) params.wired = wired;
- if (date_s !== undefined) params.date_s = date_s;
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { orders: [] } as T,
- })
- }
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const reserveDetailFetcher = useCallback(
- function reserveDetailFetcherImpl<T>(
- endpoint: string,
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- params: {
- rewards: "yes",
+
+export function useBackendRequest(): useBackendInstanceRequestType {
+ const {url: rootBackendUrl} = useBackendContext();
+ // const {id} = useInstanceContext();
+ const {request: requestHandler} = useApiContext();
+
+ //TODO: check
+ const baseUrl = "http://localhost:8083/";
+ const token = "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0";
+
+
+
+
+ const request = useCallback(
+ function requestImpl<T>(
+ endpoint: string,
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {...options, token});
},
- token,
- });
- },
- [baseUrl, token],
- );
-
- const rewardsDetailFetcher = useCallback(
- function rewardsDetailFetcherImpl<T>(
- endpoint: string,
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- params: {
- pickups: "yes",
+ [baseUrl],
+ );
+
+ const multiFetcher = useCallback(
+ function multiFetcherImpl<T>(
+ params: string[],
+ options: RequestOptions = {},
+ ): Promise<HttpResponseOk<T>[]> {
+ return Promise.all(
+ params.map((endpoint) =>
+ requestHandler<T>(baseUrl, endpoint, {...options, token}),
+ ),
+ );
},
- token,
- });
- },
- [baseUrl, token],
- );
-
- const transferFetcher = useCallback(
- function transferFetcherImpl<T>(
- args: [endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,]
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, payto_uri, verified, position, delta] = args
- const params: any = {};
- if (payto_uri !== undefined) params.payto_uri = payto_uri;
- if (verified !== undefined) params.verified = verified;
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { transfers: [] } as T,
- })
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const templateFetcher = useCallback(
- function templateFetcherImpl<T>(
- args: [endpoint: string,
- position?: string,
- delta?: number,]
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args
- const params: any = {};
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { templates: [] } as T,
- })
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const webhookFetcher = useCallback(
- function webhookFetcherImpl<T>(
- args: [endpoint: string,
- position?: string,
- delta?: number,]
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args
- const params: any = {};
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { webhooks: [] } as T,
- })
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- return {
- request,
- fetcher,
- multiFetcher,
- orderFetcher,
- reserveDetailFetcher,
- rewardsDetailFetcher,
- transferFetcher,
- templateFetcher,
- webhookFetcher,
- };
-}
+ [baseUrl],
+ );
+
+ const fetcher = useCallback(
+ function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
+ return requestHandler<T>(baseUrl, endpoint, {token});
+ },
+ [baseUrl],
+ );
+
+ const depositConfirmationFetcher = useCallback(
+ function orderFetcherImpl<T>(
+ args: [endpoint: string,
+ ],
+ ): Promise<HttpResponseOk<T>> {
+ const [endpoint] = args;
+ const params: any = {"token": "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"};
+ return requestHandler<T>(baseUrl, endpoint, {params, token});
+ },
+ [baseUrl],
+ );
+
+
+ return {
+ request,
+ fetcher,
+ depositConfirmationFetcher,
+ multiFetcher
+ };
+} \ No newline at end of file
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..6a25d3037
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/critical.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ HttpResponsePaginated,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { AuditorBackend, WithId } from "../declaration.js";
+import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
+import { useBackendRequest, 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;
+
+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 };
+} \ No newline at end of file
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..ae62da35e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/entity.ts
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, WithId } from "../declaration.js";
+import { useBackendRequest, useMatchMutate } from "./backend.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, useSWRConfig } from "swr";
+import { useEntityContext } from "../context/entity.js";
+
+const useSWR = _useSWR as unknown as SWRHook;
+
+type YesOrNo = "yes" | "no";
+
+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 };
+} \ No newline at end of file
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..97bf2577f
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/finance.ts
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, WithId } from "../declaration.js";
+import { useBackendRequest, 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 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..cf1c57771 100644
--- a/packages/auditor-backoffice-ui/src/hooks/index.ts
+++ b/packages/auditor-backoffice-ui/src/hooks/index.ts
@@ -14,138 +14,66 @@
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 {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,
+ url?: string,
): [string, StateUpdater<string>] {
- const [value, setter] = useSimpleLocalStorage(
- "auditor-base-url",
- url || calculateRootPath(),
- );
-
- const checkedSetter = (v: ValueOrFunction<string>) => {
- return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
- };
-
- return [value!, checkedSetter];
-}
+ const [value, setter] = useSimpleLocalStorage(
+ "auditor-base-url",
+ url || calculateRootPath(),
+ );
-export function useBackendDefaultToken(
-): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
- const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+ const checkedSetter = (v: ValueOrFunction<string>) => {
+ return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
+ };
- 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];
+ return [value!, checkedSetter];
}
-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>];
-}
+const calculateRootPath = () => {
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
+
+ /**
+ * 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("/webui/", "");
+};
export function useSimpleLocalStorage(
- key: string,
- initialValue?: string,
+ key: string,
+ initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
- const [storedValue, setStoredValue] = useState<string | undefined>(
- (): string | undefined => {
- return typeof window !== "undefined"
- ? window.localStorage.getItem(key) || initialValue
- : initialValue;
- },
- );
-
- const setValue = (
- value?: string | ((val?: string) => string | undefined),
- ) => {
- setStoredValue((p) => {
- const toStore = value instanceof Function ? value(p) : value;
- if (typeof window !== "undefined") {
- if (!toStore) {
- window.localStorage.removeItem(key);
- } else {
- window.localStorage.setItem(key, toStore);
- }
- }
- return toStore;
- });
- };
-
- return [storedValue, setValue];
-}
+ const [storedValue, setStoredValue] = useState<string | undefined>(
+ (): string | undefined => {
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
+ },
+ );
+
+ const setValue = (
+ value?: string | ((val?: string) => string | undefined),
+ ) => {
+ setStoredValue((p) => {
+ const toStore = value instanceof Function ? value(p) : value;
+ if (typeof window !== "undefined") {
+ if (!toStore) {
+ window.localStorage.removeItem(key);
+ } else {
+ window.localStorage.setItem(key, toStore);
+ }
+ }
+ return toStore;
+ });
+ };
+
+ return [storedValue, setValue];
+} \ No newline at end of file
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..89524f24e
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/hooks/operational.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/>
+ */
+import {
+ HttpResponse,
+ HttpResponseOk,
+ RequestError,
+} from "@gnu-taler/web-util/browser";
+import { AuditorBackend, WithId } from "../declaration.js";
+import { useBackendRequest, 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;
+
+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>;
+} \ No newline at end of file
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/index.tsx b/packages/auditor-backoffice-ui/src/index.tsx
index 7fdf7c1c3..fc956e8aa 100644
--- a/packages/auditor-backoffice-ui/src/index.tsx
+++ b/packages/auditor-backoffice-ui/src/index.tsx
@@ -21,4 +21,4 @@ import "./scss/main.scss";
const app = document.getElementById("app");
-render(<Application />, app as any);
+render(<Application />, app as any); \ No newline at end of file
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/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
deleted file mode 100644
index fdae1a24d..000000000
--- a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts
+++ /dev/null
@@ -1,18 +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/>
- */
-
-// export * as list from "./list/stories.js";
-export * as create from "./create/stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
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..9bb75907d
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/default/Table.tsx
@@ -0,0 +1,155 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { StateUpdater, 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>
+ );
+} \ No newline at end of file
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..1b7758190
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/default/index.tsx
@@ -0,0 +1,130 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a 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 {
+ 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 { Notification } from "../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { EntityDataContextProvider, useEntityContext } from "../../context/entity.js";
+import { getEntityList, useEntityAPI } from "../../hooks/entity.js";
+import { useMemo } from "preact/hooks";
+import { ConfirmModal, DeleteModal } from "../../components/modal/index.js";
+import { route } from "preact-router";
+import { Paths } from "../../InstanceRoutes.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..60ae7b578
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/details/ListPage.tsx
@@ -0,0 +1,346 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General 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, Fragment } from "preact";
+import { route, Route } from "preact-router";
+import { Paths, Redirect } from "../../InstanceRoutes.js";
+import { AuditorBackend } from "../../declaration.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/context/instance.ts b/packages/auditor-backoffice-ui/src/paths/details/index.tsx
index 5800ade7e..f99dae7e5 100644
--- a/packages/auditor-backoffice-ui/src/context/instance.ts
+++ b/packages/auditor-backoffice-ui/src/paths/details/index.tsx
@@ -16,21 +16,24 @@
/**
*
+ * @author Nic Eigel
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { createContext } from "preact";
-import { useContext } from "preact/hooks";
-import { LoginToken } from "../declaration.js";
+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";
-interface Type {
- id: string;
- token?: LoginToken;
- admin?: boolean;
- changeToken: (t?: LoginToken) => void;
-}
+export default function DetailsDashboard(): VNode {
-const Context = createContext<Type>({} as any);
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
-export const InstanceContextProvider = Context.Provider;
-export const useInstanceContext = (): Type => useContext(Context);
+ return (
+ <section class="section is-main-section">
+ <NotificationCard notification={notif} />
+ <ListPage />
+ </section>
+ );
+} \ No newline at end of file
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..88ca6bcfd
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/finance/ListPage.tsx
@@ -0,0 +1,214 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a 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 { h, VNode, Fragment } from "preact";
+
+export function ListPage(data: any): VNode {
+ const { i18n } = useTranslationContext();
+
+ 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/deposit_confirmations/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/finance/index.tsx
index 8e0f7647f..b0d07aa0f 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/update/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,43 +16,41 @@
/**
*
+ * @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 { 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 { Loading } from "../../components/exception/loading.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { getKeyFiguresData } from "../../hooks/finance.js";
+
-export type Entity = MerchantBackend.Products.ProductAddDetail;
interface Props {
- onBack?: () => void;
- onConfirm: () => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
- onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- pid: string;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
}
-export default function UpdateProduct({
- pid,
- onConfirm,
- onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
-}: Props): VNode {
- const { updateProduct } = useProductAPI();
- const result = useProductDetails(pid);
+
+export default function FinanceDashboard({
+ onUnauthorized,
+ // onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+ }: Props): VNode {
+
+ const result = getKeyFiguresData();
+
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -69,27 +67,14 @@ export default function UpdateProduct({
result.status === HttpStatusCode.NotFound
)
return onNotFound();
- return onLoadError(result);
+ else
+ return onNotFound();
}
return (
- <Fragment>
+ <section class="section is-main-section">
<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>
+ <ListPage data={result} />
+ </section>
);
-}
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
deleted file mode 100644
index 3336c53a4..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/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/Accounts/Create",
- component: TestedComponent,
-};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
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/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
deleted file mode 100644
index 6b4b63735..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/accounts/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/Accounts/List",
- component: TestedComponent,
-};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
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/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/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
deleted file mode 100644
index 1d8c76ff9..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/index.stories.ts
+++ /dev/null
@@ -1,19 +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/>
- */
-
-export * as details from "./details/stories.js";
-export * as kycList from "./kyc/list/ListPage.stories.js";
-export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js";
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx
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/kyc/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
deleted file mode 100644
index 5b93ac169..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ /dev/null
@@ -1,63 +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 { Loading } from "../../../../components/exception/loading.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
-import { ListPage } from "./ListPage.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-
-interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- onNotFound: () => VNode;
-}
-
-export default function ListKYC({
- onUnauthorized,
- onLoadError,
- onNotFound,
-}: Props): VNode {
- const result = useInstanceKYCDetails();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
- }
-
- const status = result.data.type === "ok" ? undefined : result.data.status;
-
- if (!status) {
- return <div>no kyc required</div>;
- }
- return <ListPage status={status} />;
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx
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/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx
deleted file mode 100644
index 26f851cc8..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/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/OtpDevices/Create",
- component: TestedComponent,
-};
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
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/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
deleted file mode 100644
index 648846793..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
+++ /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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useWebhookAPI } from "../../../../hooks/webhooks.js";
-import { Notification } from "../../../../utils/types.js";
-import { CreatePage } from "./CreatePage.js";
-import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
-import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
-
-export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
-interface Props {
- onBack?: () => void;
- onConfirm: () => void;
-}
-
-export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { createOtpDevice } = useOtpDeviceAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { i18n } = useTranslationContext();
- const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null)
-
- if (created) {
- return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
- }
-
- return (
- <>
- <NotificationCard notification={notif} />
- <CreatePage
- onBack={onBack}
- onCreate={(request: Entity) => {
- return createOtpDevice(request)
- .then((d) => {
- setCreated(request)
- })
- .catch((error) => {
- setNotif({
- message: i18n.str`could not create device`,
- type: "ERROR",
- description: error.message,
- });
- });
- }}
- />
- </>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx
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/Table.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
deleted file mode 100644
index 0c28027fe..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/Table.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 { StateUpdater, useState } from "preact/hooks";
-import { MerchantBackend } from "../../../../declaration.js";
-
-type Entity = MerchantBackend.OTP.OtpDeviceEntry;
-
-interface Props {
- devices: Entity[];
- onDelete: (e: Entity) => void;
- onSelect: (e: Entity) => void;
- onCreate: () => void;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
-}
-
-export function CardTable({
- devices,
- onCreate,
- onDelete,
- onSelect,
- onLoadMoreAfter,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
-}: Props): VNode {
- const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
-
- const { i18n } = useTranslationContext();
-
- return (
- <div class="card has-table">
- <header class="card-header">
- <p class="card-header-title">
- <span class="icon">
- <i class="mdi mdi-newspaper" />
- </span>
- <i18n.Translate>OTP Devices</i18n.Translate>
- </p>
- <div class="card-header-icon" aria-label="more options">
- <span
- class="has-tooltip-left"
- data-tooltip={i18n.str`add new devices`}
- >
- <button class="button is-info" type="button" onClick={onCreate}>
- <span class="icon is-small">
- <i class="mdi mdi-plus mdi-36px" />
- </span>
- </button>
- </span>
- </div>
- </header>
- <div class="card-content">
- <div class="b-table has-pagination">
- <div class="table-wrapper has-mobile-cards">
- {devices.length > 0 ? (
- <Table
- instances={devices}
- onDelete={onDelete}
- onSelect={onSelect}
- rowSelection={rowSelection}
- rowSelectionHandler={rowSelectionHandler}
- onLoadMoreAfter={onLoadMoreAfter}
- onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
- />
- ) : (
- <EmptyTable />
- )}
- </div>
- </div>
- </div>
- </div>
- );
-}
-interface TableProps {
- rowSelection: string[];
- instances: Entity[];
- onDelete: (e: Entity) => void;
- onSelect: (e: Entity) => void;
- rowSelectionHandler: StateUpdater<string[]>;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
-}
-
-function toggleSelected<T>(id: T): (prev: T[]) => T[] {
- return (prev: T[]): T[] =>
- prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
-}
-
-function Table({
- instances,
- onLoadMoreAfter,
- onDelete,
- onSelect,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
-}: TableProps): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div class="table-container">
- {hasMoreBefore && (
- <button
- class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices before the first one`}
- onClick={onLoadMoreBefore}
- >
- <i18n.Translate>load newer devices</i18n.Translate>
- </button>
- )}
- <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
- <thead>
- <tr>
- <th>
- <i18n.Translate>ID</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Description</i18n.Translate>
- </th>
- <th />
- </tr>
- </thead>
- <tbody>
- {instances.map((i) => {
- return (
- <tr key={i.otp_device_id}>
- <td
- onClick={(): void => onSelect(i)}
- style={{ cursor: "pointer" }}
- >
- {i.otp_device_id}
- </td>
- <td
- onClick={(): void => onSelect(i)}
- style={{ cursor: "pointer" }}
- >
- {i.otp_device_id}
- </td>
- <td class="is-actions-cell right-sticky">
- <div class="buttons is-right">
- <button
- class="button is-danger is-small has-tooltip-left"
- data-tooltip={i18n.str`delete selected devices from the database`}
- onClick={() => onDelete(i)}
- >
- Delete
- </button>
- </div>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- {hasMoreAfter && (
- <button
- class="button is-fullwidth"
- data-tooltip={i18n.str`load more devices after the last one`}
- onClick={onLoadMoreAfter}
- >
- <i18n.Translate>load older devices</i18n.Translate>
- </button>
- )}
- </div>
- );
-}
-
-function EmptyTable(): VNode {
- const { i18n } = useTranslationContext();
- return (
- <div class="content has-text-grey has-text-centered">
- <p>
- <span class="icon is-large">
- <i class="mdi mdi-emoticon-sad mdi-48px" />
- </span>
- </p>
- <p>
- <i18n.Translate>
- There is no devices yet, add more pressing the + sign
- </i18n.Translate>
- </p>
- </div>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
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/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx
deleted file mode 100644
index d6b1d65e0..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/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/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/products/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
deleted file mode 100644
index becaf8f3a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx
+++ /dev/null
@@ -1,80 +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.ProductAddDetail & {
- product_id: string;
-};
-
-interface Props {
- onCreate: (d: Entity) => Promise<void>;
- onBack?: () => void;
-}
-
-export function CreatePage({ onCreate, onBack }: Props): VNode {
- const [submitForm, addFormSubmitter] = useListener<Entity | undefined>(
- (result) => {
- if (result) return onCreate(result);
- return Promise.reject();
- },
- );
-
- const { i18n } = useTranslationContext();
-
- return (
- <div>
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <ProductForm onSubscribe={addFormSubmitter} />
-
- <div class="buttons is-right mt-5">
- {onBack && (
- <button class="button" onClick={onBack}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- )}
- <AsyncButton
- onClick={submitForm}
- data-tooltip={
- !submitForm
- ? i18n.str`Need to complete marked fields`
- : "confirm operation"
- }
- disabled={!submitForm}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </div>
- </div>
- <div class="column" />
- </div>
- </section>
- </div>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx
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/reserves/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx
deleted file mode 100644
index 5542c028a..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/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/Reserve/Create",
- component: TestedComponent,
- argTypes: {
- onCreate: { action: "onCreate" },
- onBack: { action: "onBack" },
- },
-};
-
-function createExample<Props>(
- Component: FunctionalComponent<Props>,
- props: Partial<Props>,
-) {
- const r = (args: any) => <Component {...args} />;
- r.args = props;
- return r;
-}
-
-export const Example = createExample(TestedComponent, {});
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
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/details/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.tsx
deleted file mode 100644
index 8e2a74529..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/reserves/details/index.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 { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { Loading } from "../../../../components/exception/loading.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useReserveDetails } from "../../../../hooks/reserves.js";
-import { DetailPage } from "./DetailPage.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-
-interface Props {
- rid: string;
-
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- onNotFound: () => VNode;
- onDelete: () => void;
- onBack: () => void;
-}
-export default function DetailReserve({
- rid,
- onUnauthorized,
- onLoadError,
- onNotFound,
- onBack,
- onDelete,
-}: Props): VNode {
- const result = useReserveDetails(rid);
-
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
- }
- return (
- <Fragment>
- <DetailPage selected={result.data} onBack={onBack} id={rid} />
- </Fragment>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/auditor-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
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/Update.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx
deleted file mode 100644
index 8d07cb31f..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/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/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/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.tsx
deleted file mode 100644
index 25551a031..000000000
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/create/index.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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { NotificationCard } from "../../../../components/menu/index.js";
-import { MerchantBackend } from "../../../../declaration.js";
-import { useInstanceDetails } from "../../../../hooks/instance.js";
-import { useTransferAPI } from "../../../../hooks/transfer.js";
-import { Notification } from "../../../../utils/types.js";
-import { CreatePage } from "./CreatePage.js";
-import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js";
-
-export type Entity = MerchantBackend.Transfers.TransferInformation;
-interface Props {
- onBack?: () => void;
- onConfirm: () => void;
-}
-
-export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { informTransfer } = useTransferAPI();
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { i18n } = useTranslationContext();
- const instance = useInstanceBankAccounts();
- const accounts = !instance.ok
- ? []
- : instance.data.accounts.map((a) => a.payto_uri);
-
- return (
- <>
- <NotificationCard notification={notif} />
- <CreatePage
- onBack={onBack}
- accounts={accounts}
- onCreate={(request: MerchantBackend.Transfers.TransferInformation) => {
- return informTransfer(request)
- .then(() => onConfirm())
- .catch((error) => {
- setNotif({
- message: i18n.str`could not inform transfer`,
- type: "ERROR",
- description: error.message,
- });
- });
- }}
- />
- </>
- );
-}
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/auditor-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx
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/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..c99dc6050 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,187 +16,198 @@
/**
*
+ * @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 { useCallback, useState } from "preact/hooks";
+import { useBackendContext, useBackendTokenContext } from "../../context/backend.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { Notification } from "../../utils/types.js";
+import { useBackendToken } from "../../hooks/backend.js";
+import { Route } from "preact-router";
+import { Paths, Redirect } from "../../InstanceRoutes.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 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 });
+
+ const result = useBackendToken();
+ if (!result.ok) {
+ }
+ if (result.ok) {
+ //TODO fixme
+ const { token } = useBackendTokenContext();
+ /* return (
+ <Route path="/" component={Redirect} to={Paths.key_figures}/>
+ );*/
} else {
- onConfirm(undefined);
+ setNotif({
+ message: "Your password is incorrect",
+ type: "ERROR",
+ });
}
- }, [id, token])
-
- if (admin && id !== "default") {
- //admin trying to access another instance
- return (<div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <div class="modal-card" style={{ width: "100%", margin: 0 }}>
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">{i18n.str`Login required`}</p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <p>
- <i18n.Translate>Need the access token for the instance.</i18n.Translate>
- </p>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Access Token</i18n.Translate>
- </label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="password"
- placeholder={"current access token"}
- name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? doLogin()
- : null
- }
- value={token}
- onInput={(e): void => setToken(e?.currentTarget.value)}
- />
- </p>
- </div>
+ }, [token]);
+
+ return (
+ <Route path="/" component={Redirect} to={Paths.key_figures}/>
+ );
+
+ 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`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 API.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
</div>
</div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "flex-end",
- border: "1px solid",
- borderTop: 0,
- }}
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={() => doLogin()}
>
- <AsyncButton
- onClick={doLogin}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </footer>
- </div>
+ <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>
- <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>
+ return (<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>
</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)
+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)
+ setRunning(true);
onClick().then(() => {
- setRunning(false)
+ setRunning(false);
}).catch(() => {
- setRunning(false)
- })
+ setRunning(false);
+ });
}}>
{children}
- </button>
+ </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..114b95219 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
@@ -23,12 +23,12 @@ import { h, VNode } from "preact";
import { Link } from "preact-router";
export default function NotFoundPage(): VNode {
- return (
- <div>
- <p>That page doesn&apos;t exist.</p>
- <Link href="/">
- <h4>Back to Home</h4>
- </Link>
- </div>
- );
+ return (
+ <div>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
}
diff --git a/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx
new file mode 100644
index 000000000..7f0579b2b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/operations/ListPage.tsx
@@ -0,0 +1,72 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode, Fragment } 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>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>
+ );
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/auditor-backoffice-ui/src/paths/operations/index.tsx
index 8e0f7647f..c05b271fe 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/update/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,43 +16,41 @@
/**
*
+ * @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 { 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 { Loading } from "../../components/exception/loading.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { getOperationData } from "../../hooks/operational.js";
+
-export type Entity = MerchantBackend.Products.ProductAddDetail;
interface Props {
- onBack?: () => void;
- onConfirm: () => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
- onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- pid: string;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
}
-export default function UpdateProduct({
- pid,
- onConfirm,
- onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
-}: Props): VNode {
- const { updateProduct } = useProductAPI();
- const result = useProductDetails(pid);
+
+export default function OperationsDashboard({
+ onUnauthorized,
+ // onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+ }: Props): VNode {
+
+ const result = getOperationData();
+
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -69,27 +67,14 @@ export default function UpdateProduct({
result.status === HttpStatusCode.NotFound
)
return onNotFound();
- return onLoadError(result);
+ else
+ return onNotFound();
}
return (
- <Fragment>
+ <section class="section is-main-section">
<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>
+ <ListPage data={result} />
+ </section>
);
-}
+} \ No newline at end of file
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..74f83bd4a
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/security/ListPage.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Nic Eigel
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode, Fragment } 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/templates/qr/index.tsx b/packages/auditor-backoffice-ui/src/paths/security/index.tsx
index 7db7478f7..99c98a5e7 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/templates/qr/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,46 +16,43 @@
/**
*
+ * @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 { 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 { Loading } from "../../components/exception/loading.js";
+import { NotificationCard } from "../../components/menu/index.js";
+import { Notification } from "../../utils/types.js";
+import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
-
-export type Entity = MerchantBackend.Transfers.TransferInformation;
+import { getCriticalData } from "../../hooks/critical.js";
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,
- onUnauthorized,
-}: Props): VNode {
- const result = useTemplateDetails(tid);
+export default function SecurityDashboard({
+ onUnauthorized,
+ // onLoadError,
+ onCreate,
+ onSelect,
+ onNotFound,
+ }: Props): VNode {
+
+ const result = getCriticalData();
+
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
if (result.loading) return <Loading />;
if (!result.ok) {
if (
@@ -68,13 +65,14 @@ 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>
);
-}
+} \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
index 093c3d09d..77a56a794 100644
--- a/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/auditor-backoffice-ui/src/paths/settings/index.tsx
@@ -1,8 +1,28 @@
+/*
+ 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 { 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";
@@ -16,7 +36,7 @@ function getBrowserLang(): string | undefined {
export function Settings({ onClose }: { onClose?: () => void }): VNode {
const { i18n } = useTranslationContext()
const borwserLang = getBrowserLang()
- //const { update } = useLang()
+ const { update } = useLang(undefined, {})
const [value, updateValue] = useSettings()
const errors: FormErrors<Settings> = {
@@ -60,38 +80,13 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
data-tooltip={i18n.str`generate random secret key`}
class="button is-info mr-2"
onClick={(e) => {
- //update(borwserLang.substring(0, 2))
+ update(borwserLang.substring(0, 2))
}}
>
<i18n.Translate>Set default</i18n.Translate>
</button>}
</div>
</div>
- <InputToggle<Settings>
- label={i18n.str`Advance order creation`}
- tooltip={i18n.str`Shows more options in the order creation form`}
- name="advanceOrderMode"
- />
- <InputSelector<Settings>
- name="dateFormat"
- label={i18n.str`Date format`}
- expand={true}
- help={
- value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : ""
- }
- toStr={(e) => {
- if (e === "ymd") return "year month day"
- if (e === "mdy") return "month day year"
- if (e === "dmy") return "day month year"
- return "choose one"
- }}
- values={[
- "ymd",
- "mdy",
- "dmy",
- ]}
- tooltip={i18n.str`how the date is going to be displayed`}
- />
</FormProvider>
</div>
</div>
diff --git a/packages/auditor-backoffice-ui/src/scss/_aside.scss b/packages/auditor-backoffice-ui/src/scss/_aside.scss
index e0922093b..b7b59516b 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
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/sw.js b/packages/auditor-backoffice-ui/src/sw.js
deleted file mode 100644
index bf52db6fa..000000000
--- a/packages/auditor-backoffice-ui/src/sw.js
+++ /dev/null
@@ -1,25 +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 { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
-
-// setupRouting();
-// setupPrecaching(getFiles());
diff --git a/packages/auditor-backoffice-ui/src/utils/amount.ts b/packages/auditor-backoffice-ui/src/utils/amount.ts
index 475489d3e..0796087ac 100644
--- a/packages/auditor-backoffice-ui/src/utils/amount.ts
+++ b/packages/auditor-backoffice-ui/src/utils/amount.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
@@ -12,13 +12,13 @@
You should have received 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";
+import { AuditorBackend } from "../declaration.js";
/**
* merge refund with the same description and a difference less than one minute
@@ -26,7 +26,7 @@ import { MerchantBackend } from "../declaration.js";
* @param cur new refund to add to the list
* @returns list with the new refund, may be merged with the last
*/
-export function mergeRefunds(
+/*export function mergeRefunds(
prev: MerchantBackend.Orders.RefundDetails[],
cur: MerchantBackend.Orders.RefundDetails,
): MerchantBackend.Orders.RefundDetails[] {
@@ -69,3 +69,4 @@ export function rate(a: AmountJson, b: AmountJson): number {
function toFloat(amount: AmountJson): number {
return amount.value + amount.fraction / amountFractionalBase;
}
+*/ \ No newline at end of file
diff --git a/packages/auditor-backoffice-ui/src/utils/constants.ts b/packages/auditor-backoffice-ui/src/utils/constants.ts
index 7c4e288b3..e8db927c4 100644
--- a/packages/auditor-backoffice-ui/src/utils/constants.ts
+++ b/packages/auditor-backoffice-ui/src/utils/constants.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/utils/regex.test.ts b/packages/auditor-backoffice-ui/src/utils/regex.test.ts
index 984f1a472..78f2ef5ae 100644
--- a/packages/auditor-backoffice-ui/src/utils/regex.test.ts
+++ b/packages/auditor-backoffice-ui/src/utils/regex.test.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/utils/table.ts b/packages/auditor-backoffice-ui/src/utils/table.ts
index db2b2021c..1322ad804 100644
--- a/packages/auditor-backoffice-ui/src/utils/table.ts
+++ b/packages/auditor-backoffice-ui/src/utils/table.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
@@ -13,14 +13,14 @@
You should have received 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";
@@ -40,7 +40,7 @@ export function buildActions<T extends WithId>(
.filter(notEmpty)
.map((id) => ({ element: id, type: action }));
}
-
+*/
/**
* For any object or array, return the same object if is not empty.
* not empty:
@@ -48,10 +48,10 @@ export function buildActions<T extends WithId>(
* - 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..f96606a16 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
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
index db89e58be..65281bf2b 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
index 43d43a3f2..b18aad9ad 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -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/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/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..2dce76a62 100644
--- a/packages/bank-ui/src/pages/OperationState/index.ts
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -41,6 +41,7 @@ export interface Props {
onAuthorizationRequired: () => void;
routeClose: RouteDefinition;
onAbort: () => void;
+ focus?: boolean;
routeHere: RouteDefinition<{ wopid: string }>;
}
@@ -79,6 +80,7 @@ export namespace State {
status: "ready";
error: undefined;
uri: WithdrawUriResult;
+ focus?: boolean;
onAbort: () => Promise<
TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
>;
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index 32d4fea7a..6856f5f59 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 {
@@ -182,7 +185,7 @@ export function useComponentState({
}
if (data.status === "confirmed") {
- if (!settings.showWithdrawalSuccess) {
+ if (!preference.showWithdrawalSuccess) {
updateBankState("currentWithdrawalOperationId", undefined);
// onClose()
}
@@ -199,6 +202,7 @@ export function useComponentState({
error: undefined,
uri: parsedUri,
routeClose,
+ focus,
onAbort: !creds
? async () => {
onAbort();
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx
index 62308eca6..88f34ae26 100644
--- a/packages/bank-ui/src/pages/OperationState/views.tsx
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -37,6 +37,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 { doAutoFocus } from "../PaytoWireTransferForm.js";
export function InvalidPaytoView({ payto }: State.InvalidPayto) {
return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
@@ -338,7 +339,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();
@@ -387,60 +392,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..9529e3843 100644
--- a/packages/bank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -48,7 +48,7 @@ function ShowOperationPendingTag({
if (!loading && !pending && onOperationAlreadyCompleted) {
onOperationAlreadyCompleted();
}
- }, [pending]);
+ }, [loading, pending]);
if (error || !pending) {
return <Fragment />;
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 90b41d331..0fb8c0ac1 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() {
@@ -627,6 +633,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">
@@ -800,7 +817,6 @@ export function RenderAmount({
function validateRawPayto(
parsed: PaytoUri,
limit: AmountJson,
- fee: AmountJson,
host: string,
i18n: InternationalizationAPI,
type: "iban" | "x-taler-bank",
@@ -844,7 +860,7 @@ 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) {
@@ -860,7 +876,6 @@ function validateRawPayto(
function validateAmount(
amount: AmountJson,
limit: AmountJson,
- fee: AmountJson,
i18n: InternationalizationAPI,
): TranslatedString | undefined {
if (amount.currency !== limit.currency) {
@@ -869,8 +884,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/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
index 624890468..e4ca13ed6 100644
--- a/packages/bank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -206,7 +206,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":
@@ -694,10 +699,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..6b1043424 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -46,6 +46,7 @@ import {
RenderAmount,
doAutoFocus,
} from "./PaytoWireTransferForm.js";
+import { useSettingsContext } from "../context/settings.js";
const RefAmount = forwardRef(InputAmount);
@@ -65,7 +66,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,7 +82,7 @@ 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();
@@ -143,7 +145,7 @@ function OldWithdrawalForm({
if (!parsedAmount || !creds) return;
await handleError(async () => {
const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest =
- settings.fastWithdrawal
+ preference.fastWithdrawalForm
? {
suggested_amount: Amounts.stringify(parsedAmount),
}
@@ -241,9 +243,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,7 +349,7 @@ 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">
@@ -357,18 +359,18 @@ export function WalletWithdrawForm({
</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 +388,7 @@ export function WalletWithdrawForm({
</Attention>
)}
- {!settings.fastWithdrawal ? (
+ {!pref.fastWithdrawalForm ? (
<OldWithdrawalForm
focus={focus}
routeOperationDetails={routeOperationDetails}
@@ -397,6 +399,7 @@ export function WalletWithdrawForm({
/>
) : (
<OperationState
+ focus={focus}
currency={limit.currency}
onAuthorizationRequired={onAuthorizationRequired}
routeClose={routeCancel}
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..25fa36d55 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;
@@ -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/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
index b61f0cc8f..fc6d20631 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,
@@ -122,6 +123,7 @@ export function WithdrawalQRCode({
</div>
);
}
+ const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
if (data.status === "confirmed") {
return (
@@ -161,13 +163,20 @@ 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>Close</i18n.Translate>
+ </a>
+ <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>Done</i18n.Translate>
+ <i18n.Translate>Go to wallet</i18n.Translate>
</a>
</div>
</div>
@@ -239,7 +248,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..f602c67df 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -22,7 +22,7 @@ import {
TalerErrorCode,
TranslatedString,
assertUnreachable,
- parsePaytoUri
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
CopyButton,
@@ -43,6 +43,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 +69,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 +91,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) {
@@ -202,16 +209,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>
@@ -339,15 +348,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 +374,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 +456,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 +483,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 +513,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 +546,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 +567,3 @@ export function ShowAccountDetails({
</Fragment>
);
}
-
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
index dbeebf719..74e39112d 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) {
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/challenger-ui/package.json b/packages/challenger-ui/package.json
index 7cc73771b..d4d047326 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
index 6166f159a..f7488cb8d 100644
--- a/packages/challenger-ui/src/Routing.tsx
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -23,6 +23,7 @@ import {
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 { AnswerChallenge } from "./pages/AnswerChallenge.js";
@@ -91,6 +92,9 @@ function PublicRounting(): VNode {
const location = useCurrentLocation(publicPages);
const { navigateTo } = useNavigationContext();
const { start } = useSessionState();
+ useErrorBoundary((e) => {
+ console.log("error", e);
+ });
if (location === undefined) {
return <NonceNotFound />;
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
index 2b5c5c815..02ec95107 100644
--- a/packages/challenger-ui/src/app.tsx
+++ b/packages/challenger-ui/src/app.tsx
@@ -41,6 +41,7 @@ import { strings } from "./i18n/strings.js";
import { ChallengerUiSettings, fetchSettings } from "./settings.js";
import { Frame } from "./pages/Frame.js";
import { revalidateChallengeSession } from "./hooks/challenge.js";
+
const WITH_LOCAL_STORAGE_CACHE = false;
const evictBankSwrCache: CacheEvictor<ChallengerCacheEviction> = {
@@ -50,6 +51,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/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
index 70e41bf1e..ebfa57d02 100644
--- a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -46,7 +46,7 @@ export function CheckChallengeIsUpToDate({
onNoInfo,
}: Props): VNode {
const { state, updateStatus } = useSessionState();
- const { i18n } = useTranslationContext();
+ const {i18n} = useTranslationContext();
const sessionId = sessionFromParam
? sessionFromParam
@@ -59,7 +59,7 @@ export function CheckChallengeIsUpToDate({
};
const result = useChallengeSession(nonce, sessionId);
- console.log("asd");
+
if (!sessionId) {
onNoInfo();
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/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/challenger-ui/src/declaration.d.ts
index 84cc95e72..581cbcd07 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/transfers/update/index.tsx
+++ b/packages/challenger-ui/src/declaration.d.ts
@@ -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
@@ -14,13 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { h, VNode } from "preact";
-
-export default function UpdateTransfer(): VNode {
- return <div>order transfer page</div>;
+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/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
index 846242816..224c60b9b 100644
--- a/packages/challenger-ui/src/hooks/challenge.ts
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -38,7 +38,7 @@ export function useChallengeSession(
lib: { challenger: api },
} = useChallengerApiContext();
- async function fetcher([n, c, r, s]: [string, string, string, string]) {
+ async function fetcher([n, c, r, s]: [string, string, string, string]): Promise<any> {
return await api.login(n, c, r, s);
}
const { data, error } = useSWR<
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
index ed7ea8986..4dc7e0dc1 100644
--- a/packages/challenger-ui/src/hooks/session.ts
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -15,9 +15,11 @@
*/
import {
+ AbsoluteTime,
ChallengerApi,
Codec,
buildCodecForObject,
+ codecForAbsoluteTime,
codecForBoolean,
codecForChallengeStatus,
codecForNumber,
@@ -39,8 +41,10 @@ export type SessionId = {
};
export type LastChallengeResponse = {
- attemptsLeft: number;
- nextSend: string;
+ sendCodeLeft: number;
+ changeTargetLeft: number;
+ checkPinLeft: number;
+ nextSend: AbsoluteTime;
transmitted: boolean;
};
@@ -51,8 +55,10 @@ export type SessionState = SessionId & {
};
export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
buildCodecForObject<LastChallengeResponse>()
- .property("attemptsLeft", codecForNumber())
- .property("nextSend", codecForString())
+ .property("sendCodeLeft", codecForNumber())
+ .property("changeTargetLeft", codecForNumber())
+ .property("checkPinLeft", codecForNumber())
+ .property("nextSend", codecForAbsoluteTime)
.property("transmitted", codecForBoolean())
.build("LastChallengeResponse");
@@ -60,8 +66,8 @@ export const codecForSessionState = (): Codec<SessionState> =>
buildCodecForObject<SessionState>()
.property("clientId", codecForString())
.property("redirectURL", codecForStringURL())
- .property("completedURL", codecOptional(codecForStringURL()))
.property("state", codecForString())
+ .property("completedURL", codecOptional(codecForStringURL()))
.property("lastStatus", codecOptional(codecForChallengeStatus()))
.property("lastTry", codecOptional(codecForLastChallengeResponse()))
.build("SessionState");
@@ -121,7 +127,7 @@ export function useSessionState(): SessionStateHandler {
});
return;
}
- // current status
+ // current status, FIXME: better check to know if the state changed
const ls = state.lastStatus;
if (
ls.changes_left !== st.changes_left ||
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
index 73a79c51f..2740e1bdb 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 {
+ AbsoluteTime,
ChallengerApi,
HttpStatusCode,
+ TalerProtocolTimestamp,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
@@ -41,14 +43,18 @@ type Props = {
routeAsk: RouteDefinition<{ nonce: string }>;
};
-export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props): VNode {
+export function AnswerChallenge({
+ focus,
+ nonce,
+ onComplete,
+ routeAsk,
+}: Props): VNode {
const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const { state, accepted, 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,
});
@@ -62,7 +68,9 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
: state.lastStatus.last_address["email"];
const onSendAgain =
- !state || lastEmail === undefined
+ lastEmail === undefined ||
+ state?.lastStatus == undefined ||
+ state?.lastStatus.changes_left === 0
? undefined
: withErrorHandler(
async () => {
@@ -70,12 +78,16 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
return await lib.challenger.challenge(nonce, { email: lastEmail });
},
(ok) => {
- if ("redirectURL" in ok.body) {
- completed(ok.body.redirectURL);
+ if (ok.body.type === "completed") {
+ completed(new URL(ok.body.redirect_url));
} else {
accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
+ changeTargetLeft: ok.body.attempts_left,
+ checkPinLeft: state.lastStatus?.auth_attempts_left ?? 0,
+ sendCodeLeft: state.lastStatus?.pin_transmissions_left ?? 0,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ ok.body.retransmission_time,
+ ),
transmitted: ok.body.transmitted,
});
}
@@ -84,46 +96,60 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
(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 ||
+ state?.lastStatus == undefined ||
+ state?.lastStatus.auth_attempts_left === 0
? undefined
: withErrorHandler(
async () => {
return lib.challenger.solve(nonce, { pin: pin! });
},
(ok) => {
- completed(ok.body.redirectURL as URL);
+ if (ok.body.type === "completed") {
+ completed(new URL(ok.body.redirect_url));
+ } else {
+ accepted({
+ changeTargetLeft: ok.body.addresses_left,
+ checkPinLeft: ok.body.auth_attempts_left,
+ sendCodeLeft: ok.body.pin_transmissions_left,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ state?.lastStatus?.retransmission_time ??
+ TalerProtocolTimestamp.now(),
+ ),
+ transmitted: state?.lastTry?.transmitted ?? false,
+ });
+ }
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`;
+ return i18n.str`Invalid pin.`;
}
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.`;
default:
assertUnreachable(fail);
}
@@ -164,11 +190,11 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
</Attention>
)}
</p>
- {!lastTryError ? undefined : (
+ {!state.lastStatus ? 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.
+ {state.lastStatus.auth_attempts_left} times more.
</i18n.Translate>
</p>
)}
@@ -212,8 +238,21 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
<p class="mt-3 text-sm leading-6 text-gray-400">
<i18n.Translate>
- You have {state.lastTry.attemptsLeft} attempts left.
+ We send the code {state.lastTry.checkPinLeft} more times.
</i18n.Translate>
+ {state.lastTry.checkPinLeft < 1 ? (
+ <i18n.Translate>
+ You can&#39;t check the PIN anymore.
+ </i18n.Translate>
+ ) : state.lastTry.checkPinLeft === 1 ? (
+ <i18n.Translate>
+ You can check the PIN one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can check the PIN {state.lastTry.checkPinLeft} more times.
+ </i18n.Translate>
+ )}
</p>
</div>
@@ -230,12 +269,31 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
<div class="mt-10 flex justify-between">
<div>
<a
+ data-disabled={!state.lastStatus || state.lastStatus.changes_left < 1}
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"
+ 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>Change email</i18n.Translate>
</a>
- </div>
+ {state.lastStatus === undefined ? undefined :
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the email anymore.
+ </i18n.Translate>
+ ) : state.lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the email one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the email {state.lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
+ </p>
+ }
+ </div>
<div>
<Button
type="submit"
@@ -245,6 +303,22 @@ export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props):
>
<i18n.Translate>Send code again</i18n.Translate>
</Button>
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastTry.sendCodeLeft < 1 ? (
+ <i18n.Translate>
+ We can&#39;t send you the code anymore.
+ </i18n.Translate>
+ ) : state.lastTry.sendCodeLeft === 1 ? (
+ <i18n.Translate>
+ We can send the code one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ We can send the code {state.lastTry.sendCodeLeft} more
+ times.
+ </i18n.Translate>
+ )}
+ </p>
</div>
</div>
</form>
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
index 30b50d707..dc60562b7 100644
--- a/packages/challenger-ui/src/pages/AskChallenge.tsx
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -13,7 +13,7 @@
You should have received 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 { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
Attention,
Button,
@@ -48,13 +48,15 @@ export function AskChallenge({
focus,
}: Props): VNode {
const { state, accepted, completed } = useSessionState();
+ const { lib, config } = useChallengerApiContext();
+
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 regexEmail = !config.restrictions
+ ? undefined
+ : config.restrictions["email"];
- const { lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [email, setEmail] = useState<string | undefined>();
@@ -87,12 +89,16 @@ export function AskChallenge({
return lib.challenger.challenge(nonce, { email: email! });
},
(ok) => {
- if ("redirectURL" in ok.body) {
- completed(ok.body.redirectURL);
+ if (ok.body.type === "completed") {
+ completed(new URL(ok.body.redirect_url));
} else {
accepted({
- attemptsLeft: ok.body.attempts_left,
- nextSend: ok.body.next_tx_time,
+ changeTargetLeft: ok.body.attempts_left,
+ checkPinLeft: state?.lastStatus?.auth_attempts_left ?? 0,
+ sendCodeLeft: state?.lastStatus?.pin_transmissions_left ?? 0,
+ nextSend: AbsoluteTime.fromProtocolTimestamp(
+ ok.body.retransmission_time,
+ ),
transmitted: ok.body.transmitted,
});
}
@@ -101,15 +107,15 @@ export function AskChallenge({
(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.`;
}
},
);
@@ -120,7 +126,7 @@ export function AskChallenge({
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">
@@ -211,16 +217,22 @@ export function AskChallenge({
</div>
)}
- {!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">
- <i18n.Translate>
- You can change your email address another{" "}
- {status.changes_left} times.
- </i18n.Translate>
+ {state.lastStatus === undefined ? undefined : (
+ <p class="mt-2 text-sm leading-6 text-gray-400">
+ {state.lastStatus.changes_left < 1 ? (
+ <i18n.Translate>
+ You can&#39;t change the email anymore.
+ </i18n.Translate>
+ ) : state.lastStatus.changes_left === 1 ? (
+ <i18n.Translate>
+ You can change the email one last time.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ You can change the email {state.lastStatus.changes_left}{" "}
+ more times.
+ </i18n.Translate>
+ )}
</p>
)}
</div>
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
index 612eced0b..dd2a13d8c 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 {
+ Footer,
+ Header,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useSettingsContext } from "../context/settings.js";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../context/preferences.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/idb-bridge/package.json b/packages/idb-bridge/package.json
index ce3123619..a176fadc9 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.12.2",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
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/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index bc8627312..683329245 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index 8aabdce87..babadbb6a 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.11.4",
+ "version": "0.12.2",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
index 665137415..ae9078678 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, {
@@ -91,10 +94,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 +105,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",
@@ -139,7 +142,6 @@ export const publicPages = {
const history = createHashHistory();
export function Routing(_p: Props): VNode {
- // const { i18n } = useTranslationContext();
const { state } = useSessionContext();
type GlobalNotifState =
@@ -163,81 +165,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 +182,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 +210,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
@@ -472,6 +418,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
*/}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
index ad3cb0e32..080b9508e 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
@@ -52,14 +52,18 @@ export function InputDuration<T>({
const { error, required, value: anyValue, onChange } = useField<T>(name);
let strValue = "";
- const value: Duration = anyValue
+ const value: Duration = anyValue;
if (!value) {
strValue = "";
} else if (value.d_ms === "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 +100,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 +111,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/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 4ac798afe..585894863 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -222,14 +222,15 @@ 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]);
@@ -267,17 +268,20 @@ export function InputPaytoForm<T>({
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,
- });
+ isKnown: false as const,
+ };
+
+ const str = !pto ? undefined : stringifyPaytoUri(pto);
+
useEffect(() => {
onChange(str as any);
}, [str]);
@@ -292,7 +296,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 +430,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/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 8c935f33b..89a4dc48c 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}>
@@ -68,23 +70,37 @@ export function InputToggle<T>({
<div class="field-body is-flex-grow-3">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
+ {/* {String(toBoolean(value))} */}
<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/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/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 4a1f6a9df..aeb49e81d 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -118,6 +118,16 @@ export function Sidebar({ mobile }: Props): VNode {
</span>
</a>
</li>
+ <li>
+ <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>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index a35c07ace..c13839d2d 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,6 @@ 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.transfers_list:
return `${id}: Transfers`;
case InstancePaths.transfers_new:
@@ -53,11 +55,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 +70,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..ba32950b5 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -31,6 +31,8 @@ import {
AccountLetter,
codecForAccountLetter,
PaytoString,
+ PaytoUri,
+ stringifyPaytoUri,
} from "@gnu-taler/taler-util";
interface Props {
@@ -288,6 +290,131 @@ 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`Update 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 DeleteModalProps {
element: { id: string; name: string };
onCancel: () => void;
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
index 52ac2a1fe..3d15918dc 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;
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..b4c49502d
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx
@@ -0,0 +1,140 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import * as yup from "yup";
+import { TokenFamilyCreateSchema } from "../../schemas/index.js";
+import { FormErrors, FormProvider } from "../form/FormProvider.js";
+import { Input } from "../form/Input.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
+import { InputDate } from "../form/InputDate.js";
+import { InputDuration } from "../form/InputDuration.js";
+import { InputSelector } from "../form/InputSelector.js";
+import { useSessionContext } from "../../context/session.js";
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
+
+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: "",
+ name: "",
+ description: "",
+ description_i18n: {},
+ kind: TalerMerchantApi.TokenFamilyKind.Discount,
+ duration: { d_us: "forever" },
+ valid_after: { t_s: "never" },
+ valid_before: { t_s: "never" },
+ });
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ TokenFamilyCreateSchema.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 => {
+ // HACK: Think about how this can be done better
+ return value as Entity;
+ }, [value]);
+
+ useEffect(() => {
+ onSubscribe(hasErrors ? undefined : submit);
+ }, [submit, hasErrors]);
+
+ const { state } = useSessionContext();
+ const { i18n } = useTranslationContext();
+
+ 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/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index 1baf80ba6..6f6e23b42 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -22,3 +22,1707 @@
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 MerchantBackend {
+ interface ErrorDetail {
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ code: number;
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ hint?: string;
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ detail?: string;
+
+ // Name of the parameter that was bogus (if applicable).
+ parameter?: string;
+
+ // Path to the argument that was bogus (if applicable).
+ path?: string;
+
+ // Offset of the argument that was bogus (if applicable).
+ offset?: string;
+
+ // Index of the argument that was bogus (if applicable).
+ index?: string;
+
+ // Name of the object that was bogus (if applicable).
+ object?: string;
+
+ // Name of the currency than was problematic (if applicable).
+ currency?: string;
+
+ // Expected type (if applicable).
+ type_expected?: string;
+
+ // Type that was provided instead (if applicable).
+ type_actual?: string;
+ }
+
+ // Delivery location, loosely modeled as a subset of
+ // ISO20022's PostalAddress25.
+ interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: Amount;
+ }
+
+ interface Auditor {
+ // official name
+ name: string;
+
+ // Auditor's public key
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor
+ url: string;
+ }
+ interface Exchange {
+ // the exchange's base URL
+ url: string;
+
+ // master public key of the exchange
+ master_pub: EddsaPublicKey;
+ }
+
+ interface Product {
+ // merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity: Integer;
+
+ // The unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for this product. Can be empty.
+ taxes: Tax[];
+
+ // time indicating when this product should be delivered
+ delivery_date?: TalerProtocolTimestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ interface Merchant {
+ // label for a location with the business address of the merchant
+ address: Location;
+
+ // the merchant's legal name of business
+ name: string;
+
+ // label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction: Location;
+ }
+
+ interface VersionResponse {
+ // libtool-style representation of the Merchant protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Name of the protocol.
+ name: "taler-merchant";
+
+ // Currency supported by this backend.
+ currency: string;
+ }
+ interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+ }
+ namespace Instances {
+ //POST /private/instances/$INSTANCE/auth
+ interface InstanceAuthConfigurationMessage {
+ // Type of authentication.
+ // "external": The mechant backend does not do
+ // any authentication checks. Instead an API
+ // gateway must do the authentication.
+ // "token": The merchant checks an auth token.
+ // See "token" for details.
+ method: "external" | "token";
+
+ // For method "external", this field is mandatory.
+ // The token MUST begin with the string "secret-token:".
+ // After the auth token has been set (with method "token"),
+ // the value must be provided in a "Authorization: Bearer $token"
+ // header.
+ token?: string;
+ }
+ //POST /private/instances
+ interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ id: string;
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // "Authentication" header required to authorize management access the instance.
+ // Optional, if not given authentication will be disabled for
+ // this instance (hopefully authentication checks are still
+ // done by some reverse proxy).
+ auth: InstanceAuthConfigurationMessage;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // PATCH /private/instances/$INSTANCE
+ interface InstanceReconfigurationMessage {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ // GET /private/instances
+ interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance)
+ instances: Instance[];
+ }
+
+ interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Merchant instance this response is about ($INSTANCE)
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // List of the payment targets supported by this instance. Clients can
+ // specify the desired payment target in /order requests. Note that
+ // front-ends do not have to support wallets selecting payment targets.
+ payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
+ }
+
+ //GET /private/instances/$INSTANCE
+ interface QueryInstancesResponse {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+ // DELETE /private/instances/$INSTANCE
+ interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+ }
+ interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ token: string;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+ }
+ }
+
+ namespace KYC {
+ //GET /private/instances/$INSTANCE/kyc
+ interface AccountKycRedirects {
+ // Array of pending KYCs.
+ pending_kycs: MerchantAccountKycRedirect[];
+
+ // Array of exchanges with no reply.
+ timeout_kycs: ExchangeKycTimeout[];
+ }
+ interface MerchantAccountKycRedirect {
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (as returned
+ // by the exchange's /kyc-check/ endpoint).
+ // Optional, missing if the account is blocked
+ // due to AML and not due to KYC.
+ kyc_url?: string;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // AML status of the account.
+ aml_status: number;
+
+ // Our bank wire account this is about.
+ payto_uri: string;
+ }
+ interface ExchangeKycTimeout {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // 0 if there was no response at all.
+ exchange_http_status: number;
+ }
+
+ }
+
+ namespace BankAccounts {
+
+ interface AccountAddDetails {
+
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+
+ }
+
+ type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+
+ interface NoFacadeCredentials {
+ type: "none";
+ }
+
+ interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+
+ interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ interface AccountPatchDetails {
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+
+ interface AccountsSummaryResponse {
+
+ // List of accounts that are known for the instance.
+ accounts: BankAccountEntry[];
+ }
+
+ interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // Hash over the wire details (including over the salt)
+ h_wire: HashCode;
+
+ // salt used to compute h_wire
+ salt: HashCode;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ credit_facade_credentials?: FacadeCredentials;
+
+ // true if this account is active,
+ // false if it is historic.
+ active: boolean;
+ }
+
+ }
+
+ namespace Products {
+ // POST /private/products
+ interface ProductAddDetail {
+ // product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+ // PATCH /private/products/$PRODUCT_ID
+ interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // GET /private/products
+ interface InventorySummaryResponse {
+ // List of products that are present in the inventory
+ products: InventoryEntry[];
+ }
+ interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ }
+
+ // GET /private/products/$PRODUCT_ID
+ interface ProductDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n: { [lang_tag: string]: string };
+
+ // unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: Amount;
+
+ // An optional base64-encoded product image
+ image: ImageDataUrl;
+
+ // a list of taxes paid by the merchant for one unit of this product
+ taxes: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.)
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ // POST /private/products/$PRODUCT_ID/lock
+ interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // It is suggested that clients use a timeflake for this,
+ // see https://github.com/anthonynsimon/timeflake
+ lock_uuid: UUID;
+
+ // How long does the frontend intend to hold the lock
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+ }
+
+ // DELETE /private/products/$PRODUCT_ID
+ }
+
+ namespace Orders {
+ type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+ interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: Amount;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_ec: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_hc: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: Amount;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+ }
+ interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // when was the order created
+ creation_time: Timestamp;
+
+ // Order summary text.
+ summary: string;
+
+ // Total amount of the order (to be paid by the customer).
+ total_amount: Amount;
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: Amount;
+ }
+ interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: Amount;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_ec: number;
+
+ // HTTP status code received from the exchange.
+ exchange_hc: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
+ interface OrderHistory {
+ // timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ interface OrderHistoryEntry {
+ // order ID of the transaction related to this entry.
+ order_id: string;
+
+ // row ID of the order in the database
+ row_id: number;
+
+ // when the order was created
+ timestamp: Timestamp;
+
+ // the amount of money the order is for
+ amount: Amount;
+
+ // the summary of the order
+ summary: string;
+
+ // whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // whether the order has been paid or not
+ paid: boolean;
+ }
+
+ interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all
+ order: Order;
+
+ // if set, the backend will then set the refund deadline to the current
+ // time plus the specified delay. If it's not set, refunds will not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // specifies the payment target preferred by the client. Can be used
+ // to select among the various (active) wire methods supported by the instance.
+ payment_target?: string;
+
+ // specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // manage_inventory is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping card was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: UUID[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ interface MinimalOrderDetail {
+ // Amount to be paid by the customer
+ amount: Amount;
+
+ // Short summary of the order
+ summary: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ fulfillment_url?: string;
+ }
+
+ interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!)
+ product_id: string;
+
+ // How many units of the product are requested
+ quantity: Integer;
+ }
+ interface PostOrderResponse {
+ // Order ID of the response that was just created
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ interface OutOfStockResponse {
+ // Product ID of an out-of-stock item
+ product_id: string;
+
+ // Requested quantity
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quanitity)
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+ interface RefundRequest {
+ // Amount to be refunded
+ refund: Amount;
+
+ // Human-readable refund justification
+ reason: string;
+ }
+ interface MerchantRefundResponse {
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+ }
+
+ namespace Rewards {
+ // GET /private/reserves
+ interface RewardReserveStatus {
+ // Array of all known reserves (possibly empty!)
+ reserves: ReserveStatusEntry[];
+ }
+ interface ReserveStatusEntry {
+ // Public key of the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Timestamp when it was established
+ creation_time: Timestamp;
+
+ // Timestamp when it expires
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Is this reserve active (false if it was deleted but not purged)
+ active: boolean;
+ }
+
+ interface ReserveCreateRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: Amount;
+
+ // Exchange the merchant intends to use for reward
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+ }
+ interface ReserveCreateConfirmation {
+ // Public key identifying the reserve
+ reserve_pub: EddsaPublicKey;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: WireAccount[];
+ }
+ interface RewardCreateRequest {
+ // Amount that the customer should be reward
+ amount: Amount;
+
+ // Justification for giving the reward
+ justification: string;
+
+ // URL that the user should be directed to after rewarding,
+ // will be included in the reward_token.
+ next_url: string;
+ }
+ interface RewardCreateConfirmation {
+ // Unique reward identifier for the reward that was created.
+ reward_id: HashCode;
+
+ // taler://reward URI for the reward
+ taler_reward_uri: string;
+
+ // URL that will directly trigger processing
+ // the reward when the browser is redirected to it
+ reward_status_url: string;
+
+ // when does the reward expire
+ reward_expiration: Timestamp;
+ }
+
+ interface ReserveDetail {
+ // Timestamp when it was established.
+ creation_time: Timestamp;
+
+ // Timestamp when it expires.
+ expiration_time: Timestamp;
+
+ // Initial amount as per reserve creation call.
+ merchant_initial_amount: Amount;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: Amount;
+
+ // Amount picked up so far.
+ pickup_amount: Amount;
+
+ // Amount approved for rewards that exceeds the pickup_amount.
+ committed_amount: Amount;
+
+ // Array of all rewards created by this reserves (possibly empty!).
+ // Only present if asked for explicitly.
+ rewards?: RewardStatusEntry[];
+
+ // Is this reserve active (false if it was deleted but not purged)?
+ active: boolean;
+
+ // Array of wire accounts of the exchange that could
+ // be used to fill the reserve, can be NULL
+ // if the reserve is inactive or was already filled
+ accounts?: WireAccount[];
+
+ // URL of the exchange hosting the reserve,
+ // NULL if the reserve is inactive
+ exchange_url: string;
+ }
+
+ interface RewardStatusEntry {
+ // Unique identifier for the reward.
+ reward_id: HashCode;
+
+ // Total amount of the reward that can be withdrawn.
+ total_amount: Amount;
+
+ // Human-readable reason for why the reward was granted.
+ reason: string;
+ }
+
+ interface RewardDetails {
+ // Amount that we authorized for this reward.
+ total_authorized: Amount;
+
+ // Amount that was picked up by the user already.
+ total_picked_up: Amount;
+
+ // Human-readable reason given when authorizing the reward.
+ reason: string;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ expiration: Timestamp;
+
+ // Reserve public key from which the reward is funded.
+ reserve_pub: EddsaPublicKey;
+
+ // Array showing the pickup operations of the wallet (possibly empty!).
+ // Only present if asked for explicitly.
+ pickups?: PickupDetail[];
+ }
+ interface PickupDetail {
+ // Unique identifier for the pickup operation.
+ pickup_id: HashCode;
+
+ // Number of planchets involved.
+ num_planchets: Integer;
+
+ // Total amount requested for this pickup_id.
+ requested_amount: Amount;
+ }
+ }
+
+ namespace Transfers {
+ interface TransferList {
+ // list of all the transfers that fit the filter that we know
+ transfers: TransferDetails[];
+ }
+ interface TransferDetails {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: string;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filgering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ interface TransferInformation {
+ // how much was wired to the merchant (minus fees)
+ credit_amount: Amount;
+
+ // raw wire transfer identifier identifying the wire transfer (a base32-encoded value)
+ wtid: WireTransferIdentifierRawP;
+
+ // target account that received the wire transfer
+ payto_uri: string;
+
+ // base URL of the exchange that made the wire transfer
+ exchange_url: string;
+ }
+ }
+
+ namespace OTP {
+ interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string | undefined;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+
+ }
+ namespace Template {
+ interface TemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+ interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: Amount;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: Integer;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: RelativeTime;
+ }
+ interface TemplatePatchDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+ }
+
+ interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+ }
+
+ interface TemplateDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+ }
+
+ interface UsingTemplateDetails {
+ // Subject of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: Amount;
+ }
+
+ interface UsingTemplateResponse {
+ // After enter the request. The user will be pay with a taler URL.
+ order_id: string;
+ token: string;
+ }
+ }
+
+ namespace Webhooks {
+ type MerchantWebhookType = "pay" | "refund";
+ interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: MerchantWebhookType;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookPatchDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ interface WebhookSummaryResponse {
+ // List of webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+ }
+ interface WebhookEntry {
+ // Webhook identifier, as found in the webhook.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+ }
+ interface WebhookDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+ }
+
+ namespace TokenFamilies {
+ // Kind of the token family.
+ type TokenFamilyKind = "discount" | "subscription";
+
+ // POST /private/tokenfamilies
+ interface TokenFamilyAddDetail {
+ // 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;
+ }
+
+ // PATCH /private/tokenfamilies/$SLUG
+ interface TokenFamilyPatchDetail {
+ // 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;
+ }
+
+ // GET /private/tokenfamilies
+ interface TokenFamilySummaryResponse {
+ // All configured token families of this instance.
+ token_families: TokenFamilyEntry[];
+ }
+
+ interface TokenFamilyEntry {
+ // 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;
+ }
+
+ // GET /private/tokenfamilies/$SLUG
+ interface TokenFamilyDetail {
+ // 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;
+ }
+
+ }
+
+ 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/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts
index 8857ad839..4c917fe9e 100644
--- a/packages/merchant-backoffice-ui/src/hooks/bank.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts
@@ -13,18 +13,19 @@
You should have received 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";
+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(
@@ -35,7 +36,9 @@ export function revalidateInstanceBankAccounts() {
}
export function useInstanceBankAccounts() {
const { state: session } = useSessionContext();
- const { lib: { instance } } = useSessionContext();
+ const {
+ lib: { instance },
+ } = useSessionContext();
// const [offset, setOffset] = useState<string | undefined>();
@@ -57,19 +60,24 @@ export function useInstanceBankAccounts() {
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 {
+ lib: { instance },
+ } = useSessionContext();
async function fetcher([token, wireId]: [AccessToken, string]) {
return await instance.getBankAccountDetails(token, wireId);
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
index a21d2921c..fddbc6e08 100644
--- a/packages/merchant-backoffice-ui/src/hooks/preference.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -27,6 +27,7 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
export interface Preferences {
advanceOrderMode: boolean;
+ advanceInstanceMode: boolean;
hideKycUntil: AbsoluteTime;
hideMissingAccountUntil: AbsoluteTime;
dateFormat: "ymd" | "dmy" | "mdy";
@@ -34,6 +35,7 @@ export interface Preferences {
const defaultSettings: Preferences = {
advanceOrderMode: false,
+ advanceInstanceMode: false,
hideKycUntil: AbsoluteTime.never(),
hideMissingAccountUntil: AbsoluteTime.never(),
dateFormat: "ymd",
@@ -42,6 +44,7 @@ const defaultSettings: Preferences = {
export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
.property("advanceOrderMode", codecForBoolean())
+ .property("advanceInstanceMode", codecForBoolean())
.property("hideKycUntil", codecForAbsoluteTime)
.property("hideMissingAccountUntil", codecForAbsoluteTime)
.property(
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..221babf30
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/tokenfamily.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/>
+ */
+import { MerchantBackend } from "../declaration.js";
+import { useSessionContext } from "../context/session.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook } from "swr";
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function useInstanceTokenFamilies() {
+ const { state: session, lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<number | undefined>();
+
+ async function fetcher([token, bid]: [AccessToken, number]) {
+ return await instance.listTokenFamilies(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid === undefined ? undefined: String(bid),
+ // order: "dec",
+ });
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listTokenFamilies">,
+ TalerHttpError
+ >([session.token, "offset", "listTokenFamilies"], fetcher);
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return data;
+}
+
+export function useTokenFamilyDetails(tokenFamilySlug: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([slug, token]: [string, AccessToken]) {
+ return await instance.getTokenFamilyDetails(token, slug);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getTokenFamilyDetails">,
+ TalerHttpError
+ >([tokenFamilySlug, session.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: MerchantBackend.TokenFamilies.TokenFamilyAddDetail,
+ ) => Promise<void>;
+ updateTokenFamily: (
+ slug: string,
+ data: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
+ ) => Promise<void>;
+ deleteTokenFamily: (slug: string) => Promise<void>;
+}
diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po
index 2c4bc64a7..58a3745ac 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-06-28 00:57+0000\n"
+"Last-Translator: Luis Avalos <avalos.diaz.0577@gmail.com>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/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/components/modal/index.tsx:71
#, c-format
@@ -373,9 +373,9 @@ msgid "Description"
msgstr "Descripcion"
#: src/components/form/InputSearchProduct.tsx:94
-#, fuzzy, c-format
+#, c-format
msgid "Product"
-msgstr "Productos"
+msgstr "Producto"
#: src/components/form/InputSearchProduct.tsx:95
#, c-format
@@ -746,8 +746,8 @@ 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 "
+"win interación por parte del usuario."
#: src/paths/instance/orders/create/CreatePage.tsx:502
#, c-format
@@ -775,6 +775,9 @@ 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/paths/instance/orders/create/CreatePage.tsx:512
#, c-format
@@ -788,6 +791,9 @@ msgid ""
"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/paths/instance/orders/create/CreatePage.tsx:517
#, fuzzy, c-format
@@ -802,9 +808,9 @@ msgid ""
msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:522
-#, fuzzy, c-format
+#, c-format
msgid "Minimum age required"
-msgstr "Login necesario"
+msgstr "Edad mínima requerida"
#: src/paths/instance/orders/create/CreatePage.tsx:523
#, c-format
@@ -812,11 +818,14 @@ 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
#, c-format
msgid "Min age defined by the producs is %1$s"
-msgstr ""
+msgstr "La edad mínima definida por el producto es%1$s"
#: src/paths/instance/orders/create/CreatePage.tsx:534
#, fuzzy, c-format
@@ -827,11 +836,12 @@ msgstr "Información extra"
#, 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
#, 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
@@ -854,39 +864,39 @@ msgid "seconds"
msgstr "segundos"
#: src/components/form/InputDuration.tsx:53
-#, fuzzy, c-format
+#, c-format
msgid "forever"
-msgstr "nunca"
+msgstr "por siempre"
#: src/components/form/InputDuration.tsx:62
#, c-format
msgid "%1$sM"
-msgstr ""
+msgstr "%1$sM"
#: src/components/form/InputDuration.tsx:64
#, c-format
msgid "%1$sY"
-msgstr ""
+msgstr "%1$sA"
#: src/components/form/InputDuration.tsx:66
#, c-format
msgid "%1$sd"
-msgstr ""
+msgstr "%1$sd"
#: src/components/form/InputDuration.tsx:68
#, c-format
msgid "%1$sh"
-msgstr ""
+msgstr "%1$sh"
#: src/components/form/InputDuration.tsx:70
#, c-format
msgid "%1$smin"
-msgstr ""
+msgstr "%1$smin"
#: src/components/form/InputDuration.tsx:72
#, c-format
msgid "%1$ssec"
-msgstr ""
+msgstr "%1$sseg"
#: src/paths/instance/orders/list/Table.tsx:75
#, c-format
@@ -894,9 +904,9 @@ msgid "Orders"
msgstr "Órdenes"
#: src/paths/instance/orders/list/Table.tsx:81
-#, fuzzy, c-format
+#, c-format
msgid "create order"
-msgstr "creado"
+msgstr "crear orden"
#: src/paths/instance/orders/list/Table.tsx:147
#, c-format
@@ -1027,6 +1037,7 @@ msgstr "Máxima comisión"
#, 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:93
#, c-format
@@ -1036,7 +1047,7 @@ 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 ""
+msgstr "comisión máxima por transferencia aceptada por el comerciante"
#: src/paths/instance/orders/details/DetailPage.tsx:100
#, c-format
@@ -1053,23 +1064,23 @@ msgstr "Creado en"
#: src/paths/instance/orders/details/DetailPage.tsx:106
#, c-format
msgid "time when this contract was generated"
-msgstr ""
+msgstr "momento en que se generó este contrato"
#: src/paths/instance/orders/details/DetailPage.tsx:112
#, c-format
msgid "after this deadline has passed no refunds will be accepted"
-msgstr ""
+msgstr "pasado este plazo no se aceptarán devoluciones"
#: src/paths/instance/orders/details/DetailPage.tsx:118
#, c-format
msgid ""
"after this deadline, the merchant won't accept payments for the contract"
-msgstr ""
+msgstr "pasado este plazo, el comerciante no aceptará pagos por el contrato"
#: src/paths/instance/orders/details/DetailPage.tsx:124
#, c-format
msgid "transfer deadline for the exchange"
-msgstr ""
+msgstr "plazo de transferencia para el intercambio"
#: src/paths/instance/orders/details/DetailPage.tsx:130
#, c-format
@@ -1079,7 +1090,7 @@ msgstr ""
#: src/paths/instance/orders/details/DetailPage.tsx:136
#, c-format
msgid "where the order will be delivered"
-msgstr ""
+msgstr "dónde se entregará el pedido"
#: src/paths/instance/orders/details/DetailPage.tsx:144
#, fuzzy, c-format
@@ -1091,6 +1102,8 @@ msgstr "Plazo de reembolso automático"
msgid ""
"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
@@ -1101,6 +1114,7 @@ msgstr "Información extra"
#, 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
#, c-format
@@ -1153,9 +1167,9 @@ msgid "refunded"
msgstr "reembolzado"
#: src/paths/instance/orders/details/DetailPage.tsx:480
-#, fuzzy, c-format
+#, c-format
msgid "refund order"
-msgstr "reembolzado"
+msgstr "reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:481
#, fuzzy, c-format
@@ -1170,12 +1184,12 @@ msgstr "reembolzar"
#: src/paths/instance/orders/details/DetailPage.tsx:553
#, c-format
msgid "Refunded amount"
-msgstr "Monto reembolzado"
+msgstr "Monto reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:560
-#, fuzzy, c-format
+#, c-format
msgid "Refund taken"
-msgstr "Reembolzado"
+msgstr "Reembolsado"
#: src/paths/instance/orders/details/DetailPage.tsx:570
#, fuzzy, c-format
@@ -1238,27 +1252,27 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/orders/list/ListPage.tsx:78
#, c-format
msgid "select date to show nearby orders"
-msgstr ""
+msgstr "seleccione la fecha para mostrar pedidos cercanos"
#: src/paths/instance/orders/list/ListPage.tsx:94
-#, fuzzy, c-format
+#, c-format
msgid "order id"
-msgstr "ir a id de orden"
+msgstr "ID de la orden"
#: src/paths/instance/orders/list/ListPage.tsx:100
#, c-format
msgid "jump to order with the given order ID"
-msgstr ""
+msgstr "saltar al pedido con el ID de pedido proporcionado"
#: src/paths/instance/orders/list/ListPage.tsx:122
#, c-format
msgid "remove all filters"
-msgstr ""
+msgstr "eliminar todos los filtros"
#: src/paths/instance/orders/list/ListPage.tsx:132
#, c-format
msgid "only show paid orders"
-msgstr ""
+msgstr "mostrar sólo pedidos pagados"
#: src/paths/instance/orders/list/ListPage.tsx:135
#, c-format
@@ -1281,6 +1295,8 @@ 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:155
#, c-format
@@ -1290,7 +1306,7 @@ msgstr "No transferido"
#: src/paths/instance/orders/list/ListPage.tsx:170
#, c-format
msgid "clear date filter"
-msgstr ""
+msgstr "borrar filtro de fechas"
#: src/paths/instance/orders/list/ListPage.tsx:184
#, c-format
@@ -1303,9 +1319,9 @@ msgid "Enter an order id"
msgstr "ir a id de orden"
#: src/paths/instance/orders/list/index.tsx:111
-#, fuzzy, c-format
+#, c-format
msgid "order not found"
-msgstr "Servidor no encontrado"
+msgstr "Orden no encontrada"
#: src/paths/instance/orders/list/index.tsx:178
#, fuzzy, c-format
@@ -1323,6 +1339,8 @@ 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
@@ -1332,7 +1350,7 @@ msgstr "Administrar stock"
#: src/components/form/InputStock.tsx:115
#, c-format
msgid "this product has been configured without stock control"
-msgstr ""
+msgstr "este producto se ha configurado sin control de existencias"
#: src/components/form/InputStock.tsx:119
#, c-format
@@ -1362,7 +1380,7 @@ msgstr "Actual"
#: src/components/form/InputStock.tsx:196
#, c-format
msgid "remove stock control for this product"
-msgstr ""
+msgstr "eliminar el control de existencias de este producto"
#: src/components/form/InputStock.tsx:202
#, c-format
@@ -1383,26 +1401,27 @@ msgstr "Dirección de entrega"
#, 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:139
#, c-format
msgid "illustration of the product for customers"
-msgstr ""
+msgstr "ilustración del producto para los clientes"
#: src/components/product/ProductForm.tsx:145
#, c-format
msgid "product description for customers"
-msgstr ""
+msgstr "descripción del producto para los clientes"
#: src/components/product/ProductForm.tsx:149
#, c-format
msgid "Age restricted"
-msgstr ""
+msgstr "Restricción de edad"
#: src/components/product/ProductForm.tsx:150
#, c-format
msgid "is this product restricted for customer below certain age?"
-msgstr ""
+msgstr "¿este producto está restringido para clientes menores de cierta edad?"
#: src/components/product/ProductForm.tsx:155
#, c-format
@@ -1410,12 +1429,16 @@ 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:160
#, c-format
msgid ""
"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
#, c-format
@@ -1427,16 +1450,18 @@ msgstr "Existencias"
msgid ""
"product 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"
-msgstr ""
+msgstr "impuestos incluidos en el precio del producto, expuestos a los clientes"
#: src/paths/instance/products/create/CreatePage.tsx:66
#, c-format
msgid "Need to complete marked fields"
-msgstr ""
+msgstr "Necesita completar los campos marcados"
#: src/paths/instance/products/create/index.tsx:51
#, c-format
@@ -1451,7 +1476,7 @@ msgstr "Productos"
#: src/paths/instance/products/list/Table.tsx:73
#, c-format
msgid "add product to inventory"
-msgstr ""
+msgstr "añadir producto al inventario"
#: src/paths/instance/products/list/Table.tsx:137
#, c-format
@@ -1486,27 +1511,27 @@ msgstr "Actualizar"
#: src/paths/instance/products/list/Table.tsx:260
#, c-format
msgid "remove this product from the database"
-msgstr ""
+msgstr "eliminar este producto de la base de datos"
#: src/paths/instance/products/list/Table.tsx:331
#, c-format
msgid "update the product with new price"
-msgstr ""
+msgstr "actualizar el producto con el nuevo precio"
#: src/paths/instance/products/list/Table.tsx:341
#, c-format
msgid "update product with new price"
-msgstr ""
+msgstr "actualizar producto con nuevo precio"
#: src/paths/instance/products/list/Table.tsx:399
#, c-format
msgid "add more elements to the inventory"
-msgstr ""
+msgstr "añadir más elementos al inventario"
#: src/paths/instance/products/list/Table.tsx:404
#, c-format
msgid "report elements lost in the inventory"
-msgstr ""
+msgstr "informar de elementos perdidos en el inventario"
#: src/paths/instance/products/list/Table.tsx:409
#, fuzzy, c-format
@@ -1734,7 +1759,7 @@ msgstr ""
#: src/paths/instance/reserves/list/Table.tsx:210
#, c-format
msgid "authorize new tip from selected reserve"
-msgstr ""
+msgstr "autorizar nueva punta de reserva seleccionada"
#: src/paths/instance/reserves/list/Table.tsx:237
#, fuzzy, c-format
@@ -1770,32 +1795,32 @@ msgstr "no puede ser vacío"
#: src/paths/instance/templates/create/CreatePage.tsx:100
#, c-format
msgid "to short"
-msgstr ""
+msgstr "demasiado corta"
#: src/paths/instance/templates/create/CreatePage.tsx:108
#, c-format
msgid "just letters and numbers from 2 to 7"
-msgstr ""
+msgstr "sólo letras y números del 2 al 7"
#: src/paths/instance/templates/create/CreatePage.tsx:110
#, c-format
msgid "size of the key should be 32"
-msgstr ""
+msgstr "el tamaño de la clave debe ser 32"
#: src/paths/instance/templates/create/CreatePage.tsx:137
#, c-format
msgid "Identifier"
-msgstr ""
+msgstr "Identificador"
#: src/paths/instance/templates/create/CreatePage.tsx:138
#, c-format
msgid "Name of the template in URLs."
-msgstr ""
+msgstr "Nombre de la plantilla en las URL."
#: src/paths/instance/templates/create/CreatePage.tsx:144
#, c-format
msgid "Describe what this template stands for"
-msgstr ""
+msgstr "Describa lo que representa esta plantilla"
#: src/paths/instance/templates/create/CreatePage.tsx:149
#, fuzzy, c-format
@@ -1805,7 +1830,7 @@ msgstr "Estado de orden"
#: src/paths/instance/templates/create/CreatePage.tsx:150
#, c-format
msgid "If specified, this template will create order with the same summary"
-msgstr ""
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo resumen"
#: src/paths/instance/templates/create/CreatePage.tsx:154
#, fuzzy, c-format
@@ -1815,7 +1840,7 @@ msgstr "precio unitario"
#: src/paths/instance/templates/create/CreatePage.tsx:155
#, c-format
msgid "If specified, this template will create order with the same price"
-msgstr ""
+msgstr "Si se especifica, esta plantilla creará pedidos con el mismo precio"
#: src/paths/instance/templates/create/CreatePage.tsx:159
#, c-format
@@ -1825,7 +1850,7 @@ msgstr "Edad mínima"
#: src/paths/instance/templates/create/CreatePage.tsx:161
#, c-format
msgid "Is this contract restricted to some age?"
-msgstr ""
+msgstr "¿Este contrato está restringido a alguna edad?"
#: src/paths/instance/templates/create/CreatePage.tsx:165
#, fuzzy, c-format
@@ -1838,56 +1863,58 @@ 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/templates/create/CreatePage.tsx:171
#, c-format
msgid "Verification algorithm"
-msgstr ""
+msgstr "Algoritmo de verificación"
#: src/paths/instance/templates/create/CreatePage.tsx:172
#, c-format
msgid "Algorithm to use to verify transaction in offline mode"
-msgstr ""
+msgstr "Algoritmo a utilizar para verificar la transacción en modo offline"
#: src/paths/instance/templates/create/CreatePage.tsx:180
#, c-format
msgid "Point-of-sale key"
-msgstr ""
+msgstr "Clave punto de venta"
#: src/paths/instance/templates/create/CreatePage.tsx:182
#, c-format
msgid "Useful to validate the purchase"
-msgstr ""
+msgstr "Útil para validar la compra"
#: src/paths/instance/templates/create/CreatePage.tsx:196
#, c-format
msgid "generate random secret key"
-msgstr ""
+msgstr "generar clave secreta aleatoria"
#: src/paths/instance/templates/create/CreatePage.tsx:203
#, c-format
msgid "random"
-msgstr ""
+msgstr "aleatorio"
#: src/paths/instance/templates/create/CreatePage.tsx:208
#, c-format
msgid "show secret key"
-msgstr ""
+msgstr "mostrar clave secreta"
#: src/paths/instance/templates/create/CreatePage.tsx:209
#, c-format
msgid "hide secret key"
-msgstr ""
+msgstr "ocultar clave secreta"
#: src/paths/instance/templates/create/CreatePage.tsx:216
#, c-format
msgid "hide"
-msgstr ""
+msgstr "ocultar"
#: src/paths/instance/templates/create/CreatePage.tsx:218
#, c-format
msgid "show"
-msgstr ""
+msgstr "mostrar"
#: src/paths/instance/templates/create/index.tsx:52
#, fuzzy, c-format
@@ -1902,7 +1929,7 @@ msgstr "Login necesario"
#: src/paths/instance/templates/use/UsePage.tsx:58
#, c-format
msgid "Order summary is required"
-msgstr ""
+msgstr "Se requiere resumen del pedido"
#: src/paths/instance/templates/use/UsePage.tsx:86
#, fuzzy, c-format
@@ -1912,7 +1939,7 @@ msgstr "cargar viejas transferencias"
#: src/paths/instance/templates/use/UsePage.tsx:108
#, c-format
msgid "Amount of the order"
-msgstr ""
+msgstr "Importe del pedido"
#: src/paths/instance/templates/use/UsePage.tsx:113
#, fuzzy, c-format
@@ -1930,16 +1957,19 @@ 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."
#: src/paths/instance/templates/qr/QrPage.tsx:148
-#, fuzzy, c-format
+#, c-format
msgid "Fixed amount"
-msgstr "Monto reembolzado"
+msgstr "Importe fijo"
#: src/paths/instance/templates/qr/QrPage.tsx:149
-#, fuzzy, c-format
+#, c-format
msgid "Default amount"
-msgstr "Monto reembolzado"
+msgstr "Importe por defecto"
#: src/paths/instance/templates/qr/QrPage.tsx:161
#, fuzzy, c-format
@@ -1949,27 +1979,27 @@ msgstr "Estado de orden"
#: src/paths/instance/templates/qr/QrPage.tsx:177
#, c-format
msgid "Print"
-msgstr ""
+msgstr "Imprimir"
#: src/paths/instance/templates/qr/QrPage.tsx:184
#, c-format
msgid "Setup TOTP"
-msgstr ""
+msgstr "Configurar TOTP"
#: src/paths/instance/templates/list/Table.tsx:65
#, c-format
msgid "Templates"
-msgstr ""
+msgstr "Plantillas"
#: src/paths/instance/templates/list/Table.tsx:70
#, c-format
msgid "add new templates"
-msgstr ""
+msgstr "añadir nuevas plantillas"
#: src/paths/instance/templates/list/Table.tsx:142
#, c-format
msgid "load more templates before the first one"
-msgstr ""
+msgstr "cargar más plantillas antes de la primera"
#: src/paths/instance/templates/list/Table.tsx:146
#, fuzzy, c-format
@@ -1979,12 +2009,12 @@ msgstr "cargar nuevas transferencias"
#: src/paths/instance/templates/list/Table.tsx:181
#, c-format
msgid "delete selected templates from the database"
-msgstr ""
+msgstr "eliminar las plantillas seleccionadas de la base de datos"
#: src/paths/instance/templates/list/Table.tsx:188
#, c-format
msgid "use template to create new order"
-msgstr ""
+msgstr "utilizar la plantilla para crear un nuevo pedido"
#: src/paths/instance/templates/list/Table.tsx:195
#, fuzzy, c-format
@@ -1994,7 +2024,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/templates/list/Table.tsx:210
#, c-format
msgid "load more templates after the last one"
-msgstr ""
+msgstr "cargar más plantillas después de la última"
#: src/paths/instance/templates/list/Table.tsx:214
#, fuzzy, c-format
@@ -2029,27 +2059,27 @@ 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 ""
+msgstr "Evento"
#: src/paths/instance/webhooks/create/CreatePage.tsx:90
#, 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
#, c-format
msgid "Method"
-msgstr ""
+msgstr "Método"
#: src/paths/instance/webhooks/create/CreatePage.tsx:95
#, c-format
msgid "Method used by the webhook"
-msgstr ""
+msgstr "Método utilizado por el webhook"
#: src/paths/instance/webhooks/create/CreatePage.tsx:99
#, c-format
@@ -2059,12 +2089,12 @@ msgstr "URL"
#: src/paths/instance/webhooks/create/CreatePage.tsx:100
#, c-format
msgid "URL of the webhook where the customer will be redirected"
-msgstr ""
+msgstr "URL del webhook al que se redirigirá al cliente"
#: src/paths/instance/webhooks/create/CreatePage.tsx:104
#, c-format
msgid "Header"
-msgstr ""
+msgstr "Cabecera"
#: src/paths/instance/webhooks/create/CreatePage.tsx:106
#, c-format
@@ -2074,7 +2104,7 @@ msgstr ""
#: src/paths/instance/webhooks/create/CreatePage.tsx:111
#, c-format
msgid "Body"
-msgstr ""
+msgstr "Cuerpo"
#: src/paths/instance/webhooks/create/CreatePage.tsx:112
#, c-format
@@ -2104,17 +2134,17 @@ msgstr "cargar nuevas ordenes"
#: src/paths/instance/webhooks/list/Table.tsx:151
#, c-format
msgid "Event type"
-msgstr ""
+msgstr "Tipo de evento"
#: src/paths/instance/webhooks/list/Table.tsx:176
#, c-format
msgid "delete selected webhook from the database"
-msgstr ""
+msgstr "eliminar el webhook seleccionado de la base de datos"
#: src/paths/instance/webhooks/list/Table.tsx:198
#, c-format
msgid "load more webhooks after the last one"
-msgstr ""
+msgstr "cargar más webhooks después del último"
#: src/paths/instance/webhooks/list/Table.tsx:202
#, fuzzy, c-format
@@ -2154,17 +2184,17 @@ msgstr "La URL no tiene el formato correcto"
#: src/paths/instance/transfers/create/CreatePage.tsx:98
#, c-format
msgid "Credited bank account"
-msgstr ""
+msgstr "Abono en cuenta bancaria"
#: src/paths/instance/transfers/create/CreatePage.tsx:100
#, c-format
msgid "Select one account"
-msgstr ""
+msgstr "Selecciona una cuenta"
#: src/paths/instance/transfers/create/CreatePage.tsx:101
#, c-format
msgid "Bank account of the merchant where the payment was received"
-msgstr ""
+msgstr "Cuenta bancaria del comerciante donde se recibió el pago"
#: src/paths/instance/transfers/create/CreatePage.tsx:105
#, fuzzy, c-format
@@ -2177,6 +2207,8 @@ msgid ""
"unique identifier of the wire transfer used by the exchange, must be 52 "
"characters long"
msgstr ""
+"identificador único de la transferencia utilizado por la bolsa, debe tener "
+"52 caracteres"
#: src/paths/instance/transfers/create/CreatePage.tsx:112
#, c-format
@@ -2184,16 +2216,18 @@ msgid ""
"Base URL of the exchange that made the transfer, should have been in the "
"wire transfer subject"
msgstr ""
+"URL base de la bolsa que realizó la transferencia, debería haber estado en "
+"el asunto de la transferencia bancaria"
#: src/paths/instance/transfers/create/CreatePage.tsx:117
#, c-format
msgid "Amount credited"
-msgstr ""
+msgstr "Monto abonado"
#: src/paths/instance/transfers/create/CreatePage.tsx:118
#, c-format
msgid "Actual amount that was wired to the merchant's bank account"
-msgstr ""
+msgstr "Monto real que se transfirió a la cuenta bancaria del comerciante"
#: src/paths/instance/transfers/create/index.tsx:58
#, c-format
@@ -2213,7 +2247,7 @@ msgstr "cargar nuevas transferencias"
#: src/paths/instance/transfers/list/Table.tsx:129
#, c-format
msgid "load more transfers before the first one"
-msgstr ""
+msgstr "cargar más transferencias antes de la primera"
#: src/paths/instance/transfers/list/Table.tsx:133
#, c-format
@@ -2283,7 +2317,7 @@ msgstr "Dirección de cuenta"
#: src/paths/instance/transfers/list/ListPage.tsx:100
#, c-format
msgid "only show wire transfers confirmed by the merchant"
-msgstr ""
+msgstr "mostrar sólo las transferencias confirmadas por el comerciante"
#: src/paths/instance/transfers/list/ListPage.tsx:110
#, c-format
@@ -2298,7 +2332,7 @@ msgstr "Verificado"
#: src/paths/admin/create/CreatePage.tsx:69
#, c-format
msgid "is not valid"
-msgstr ""
+msgstr "no es válido"
#: src/paths/admin/create/CreatePage.tsx:94
#, fuzzy, c-format
@@ -2388,12 +2422,12 @@ msgstr "Dirección de cuenta"
#: src/components/form/InputPaytoForm.tsx:273
#, c-format
msgid "Business Identifier Code."
-msgstr ""
+msgstr "Código de identificación de la empresa."
#: src/components/form/InputPaytoForm.tsx:282
#, c-format
msgid "Bank Account Number."
-msgstr ""
+msgstr "Número de cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:292
#, c-format
@@ -2403,17 +2437,17 @@ msgstr "Interfaz de pago unificado."
#: src/components/form/InputPaytoForm.tsx:301
#, c-format
msgid "Bitcoin protocol."
-msgstr ""
+msgstr "Protocolo Bitcoin."
#: src/components/form/InputPaytoForm.tsx:310
#, c-format
msgid "Ethereum protocol."
-msgstr ""
+msgstr "Protocolo Ethereum."
#: src/components/form/InputPaytoForm.tsx:319
#, c-format
msgid "Interledger protocol."
-msgstr ""
+msgstr "Protocolo Interledger."
#: src/components/form/InputPaytoForm.tsx:328
#, c-format
@@ -2428,17 +2462,17 @@ msgstr ""
#: src/components/form/InputPaytoForm.tsx:334
#, c-format
msgid "Bank account."
-msgstr ""
+msgstr "Cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:343
#, c-format
msgid "Bank account owner's name."
-msgstr ""
+msgstr "Nombre del titular de la cuenta bancaria."
#: src/components/form/InputPaytoForm.tsx:370
#, c-format
msgid "No accounts yet."
-msgstr ""
+msgstr "Aún no hay cuentas."
#: src/components/instance/DefaultInstanceFormFields.tsx:52
#, c-format
@@ -2446,6 +2480,8 @@ 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/components/instance/DefaultInstanceFormFields.tsx:58
#, fuzzy, c-format
@@ -2455,7 +2491,7 @@ msgstr "Nombre de edificio"
#: src/components/instance/DefaultInstanceFormFields.tsx:59
#, c-format
msgid "Legal name of the business represented by this instance."
-msgstr ""
+msgstr "Nombre legal de la empresa representada por esta instancia."
#: src/components/instance/DefaultInstanceFormFields.tsx:64
#, c-format
@@ -2475,17 +2511,17 @@ msgstr "URL de sitio web"
#: src/components/instance/DefaultInstanceFormFields.tsx:71
#, c-format
msgid "URL."
-msgstr ""
+msgstr "URL."
#: src/components/instance/DefaultInstanceFormFields.tsx:76
#, c-format
msgid "Logo"
-msgstr ""
+msgstr "Logotipo"
#: src/components/instance/DefaultInstanceFormFields.tsx:77
#, c-format
msgid "Logo image."
-msgstr ""
+msgstr "Imagen del logotipo."
#: src/components/instance/DefaultInstanceFormFields.tsx:82
#, c-format
@@ -2495,7 +2531,7 @@ msgstr "Cuenta bancaria"
#: src/components/instance/DefaultInstanceFormFields.tsx:83
#, c-format
msgid "URI specifying bank account for crediting revenue."
-msgstr ""
+msgstr "URI que especifica la cuenta bancaria para acreditar los ingresos."
#: src/components/instance/DefaultInstanceFormFields.tsx:88
#, c-format
@@ -2507,6 +2543,8 @@ msgstr "Impuesto máximo de deposito por omisión"
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/instance/DefaultInstanceFormFields.tsx:94
#, c-format
@@ -2519,6 +2557,8 @@ 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/components/instance/DefaultInstanceFormFields.tsx:100
#, c-format
@@ -2531,11 +2571,13 @@ 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/components/instance/DefaultInstanceFormFields.tsx:107
#, c-format
msgid "Physical location of the merchant."
-msgstr ""
+msgstr "Ubicación física del comerciante."
#: src/components/instance/DefaultInstanceFormFields.tsx:114
#, c-format
@@ -2557,6 +2599,8 @@ msgstr "Retrazo de pago por omisión"
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/components/instance/DefaultInstanceFormFields.tsx:129
#, c-format
@@ -2570,6 +2614,10 @@ msgid ""
"enabling it to aggregate smaller payments into larger wire transfers and "
"reducing wire fees."
msgstr ""
+"Tiempo máximo que se le permite a un intercambio retrasar la transferencia "
+"de fondos al comerciante, lo que le permite agregar pagos más pequeños en "
+"transferencias electrónicas más grandes y reducir las tarifas de "
+"transferencia."
#: src/paths/instance/update/UpdatePage.tsx:164
#, c-format
@@ -2600,7 +2648,7 @@ msgstr "Login necesario"
#: src/components/exception/login.tsx:80
#, c-format
msgid "Please enter your access token."
-msgstr ""
+msgstr "Por favor, introduzca su clave de acceso."
#: src/components/exception/login.tsx:108
#, fuzzy, c-format
@@ -2610,7 +2658,7 @@ msgstr "Acceso denegado"
#: src/InstanceRoutes.tsx:171
#, c-format
msgid "The request to the backend take too long and was cancelled"
-msgstr ""
+msgstr "La petición al backend tardó demasiado y fue cancelada"
#: src/InstanceRoutes.tsx:172
#, c-format
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..6a94109a0 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -25,7 +25,7 @@ import {
createRFC8959AccessTokenPlain,
} 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 {
@@ -34,6 +34,7 @@ import {
} from "../../../components/form/FormProvider.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";
@@ -64,13 +65,13 @@ function with_defaults(id?: string): Partial<Entity> {
}
export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
+ const [pref, updatePref] = usePreference();
+ const { i18n } = useTranslationContext();
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`
@@ -186,6 +187,34 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
</div>
<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);
+ }}
+ >
+ <a>
+ <span>
+ <i18n.Translate>Simple</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li
+ class={pref.advanceInstanceMode ? "is-active" : ""}
+ onClick={() => {
+ updatePref("advanceInstanceMode", 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">
@@ -194,7 +223,11 @@ 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>
<div class="level">
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..61f62e631 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,7 +19,16 @@
* @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 { useState } from "preact/hooks";
@@ -31,11 +40,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 {
+ CompareAccountsModal,
+ ImportingAccountModal,
+} from "../../../../components/modal/index.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { safeConvertURL } from "../update/UpdatePage.js";
+import { testRevenueAPI } from "./index.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
-type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string };
+type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean };
interface Props {
onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>;
@@ -50,6 +64,14 @@ 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,
@@ -78,13 +100,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: facadeURL.hash
? i18n.str`URL should 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 +132,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 +210,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 +250,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 () => {
+ const result = 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>
@@ -220,6 +313,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..6994c579c 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
@@ -33,7 +33,7 @@ import {
} 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";
@@ -52,6 +52,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
const { lib: api } = useSessionContext();
const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const [tested, setTested] = useState(false);
const { i18n } = useTranslationContext();
return (
@@ -60,71 +61,17 @@ 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
.addBankAccount(state.token, request)
- .then(() => {
+ .then((created) => {
+ if (created.type === "fail") {
+ setNotif({
+ message: i18n.str`could not create account`,
+ type: "ERROR",
+ description: created.detail.hint,
+ });
+ return;
+ }
onConfirm();
})
.catch((error) => {
@@ -147,12 +94,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 +124,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 +139,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
index 4ee68cd80..7e0b89f39 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -24,12 +24,12 @@ import { h, VNode } from "preact";
import { CardTable } from "./Table.js";
export interface Props {
- devices: TalerMerchantApi.BankAccountSummaryEntry[];
+ devices: TalerMerchantApi.BankAccountEntry[];
// onLoadMoreBefore?: () => void;
// onLoadMoreAfter?: () => void;
onCreate: () => void;
- onDelete: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
- onSelect: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
+ onDelete: (e: TalerMerchantApi.BankAccountEntry) => void;
+ onSelect: (e: TalerMerchantApi.BankAccountEntry) => void;
}
export function ListPage({
@@ -40,7 +40,6 @@ export function ListPage({
// onLoadMoreBefore,
// onLoadMoreAfter,
}: Props): VNode {
-
return (
<section class="section is-main-section">
<CardTable
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..0e813f4d2 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[];
@@ -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 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>
+ )}
- {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>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 />
- </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>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>
+ )}
- {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>}
+ {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>
-
);
}
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..9d09473bc 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";
@@ -40,30 +43,27 @@ interface Props {
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 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,13 +71,15 @@ 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.`
- }} />
- }
+ {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.`,
+ }}
+ />
+ )}
<ListPage
devices={result.body.accounts}
// onLoadMoreBefore={
@@ -88,8 +90,9 @@ export default function ListOtpDevices({
onSelect={(e) => {
onSelect(e.h_wire);
}}
- onDelete={(e: TalerMerchantApi.BankAccountSummaryEntry) => {
- return api.instance.deleteBankAccount(state.token, e.h_wire)
+ onDelete={(e: TalerMerchantApi.BankAccountEntry) => {
+ return api.instance
+ .deleteBankAccount(state.token, e.h_wire)
.then(() =>
setNotif({
message: i18n.str`bank account delete successfully`,
@@ -102,9 +105,8 @@ export default function ListOtpDevices({
type: "ERROR",
description: error.message,
}),
- )
- }
- }
+ );
+ }}
/>
</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..73fe43026 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,32 +40,64 @@ 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-ignore
+ 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: FormErrors<FormType> = {
+ payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
+
credit_facade_url: !state.credit_facade_url
? undefined
: !facadeURL
@@ -67,32 +108,30 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
? i18n.str`URL should not contain params`
: facadeURL.hash
? i18n.str`URL should not hash param`
+ : undefined,
+ credit_facade_credentials: !state.credit_facade_credentials
+ ? undefined
+ : undefinedIfEmpty({
+ type:
+ replacingAccountId &&
+ // @ts-ignore
+ 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(
@@ -111,21 +150,98 @@ 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-ignore
+ 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) {
+ console.log("======== REPLACE");
+ return onReplace(account, {
+ payto_uri: state.payto_uri!,
+ credit_facade_credentials,
+ credit_facade_url,
+ });
+ } else {
+ console.log("======== UPDATE");
+ 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 +249,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 +267,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`}
/>
@@ -186,13 +311,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 () => {
+ const result = await testAccountInfo();
+ }}
+ >
+ <i18n.Translate>Test</i18n.Translate>
+ </button>
+ }
+ />
</FormProvider>
<div class="buttons is-right mt-5">
@@ -217,7 +365,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..60dad7257 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;
@@ -65,7 +68,7 @@ export default function UpdateValidator({
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(result);
@@ -80,76 +83,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 api.instance
+ .updateBankAccount(state.token, bid, request)
+ .then((updated) => {
+ if (updated.type === "fail") {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: updated.detail.hint,
+ });
+ return;
+ }
+ onConfirm();
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ onReplace={async (prev, next) => {
+ try {
+ const created = await api.instance.addBankAccount(
+ state.token,
+ next,
);
- if (resp instanceof TalerError) {
+ if (created.type === "fail") {
setNotif({
- message: i18n.str`Could not create account`,
+ 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: created.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: any) {
+ setNotif({
+ message: i18n.str`could not create account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ return;
}
- return api.instance.updateBankAccount(state.token, bid, request)
- .then(onConfirm)
- .catch((error) => {
+ try {
+ const deleted = await api.instance.deleteBankAccount(
+ state.token,
+ prev.h_wire,
+ );
+ if (deleted.type === "fail") {
setNotif({
- message: i18n.str`could not update account`,
+ message: i18n.str`could not delete account`,
type: "ERROR",
- description: error.message,
+ description: deleted.detail.hint,
});
+ return;
+ }
+ } catch (error: any) {
+ setNotif({
+ message: i18n.str`could not delete account`,
+ type: "ERROR",
+ description: error.message,
});
+ return;
+ }
+ onConfirm();
}}
/>
</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..151905b5e 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
@@ -52,6 +52,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;
@@ -138,7 +139,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 || {});
@@ -360,9 +361,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 +373,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>
@@ -408,7 +409,7 @@ export function CreatePage({
inventory={instanceInventory}
/>
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<NonInventoryProductFrom
productToEdit={editingProduct}
onAddProduct={(p) => {
@@ -481,7 +482,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`}
@@ -509,13 +510,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`}
@@ -547,7 +548,7 @@ export function CreatePage({
}
/>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputDuration
name="payments.refund_deadline"
label={i18n.str`Refund time`}
@@ -580,7 +581,7 @@ export function CreatePage({
}
/>
)}
- {(settings.advanceOrderMode || noDefault_wireDeadline) && (
+ {(pref.advanceOrderMode || noDefault_wireDeadline) && (
<InputDuration
name="payments.wire_transfer_deadline"
label={i18n.str`Wire transfer time`}
@@ -614,7 +615,7 @@ export function CreatePage({
}
/>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputDuration
name="payments.auto_refund_deadline"
label={i18n.str`Auto-refund time`}
@@ -628,21 +629,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 +658,7 @@ export function CreatePage({
</InputGroup>
)}
- {settings.advanceOrderMode && (
+ {pref.advanceOrderMode && (
<InputGroup
name="extra"
label={i18n.str`Additional information`}
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..04f0b2482 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";
@@ -41,24 +46,21 @@ interface Props {
onBack?: () => void;
onConfirm: (id: string) => void;
}
-export default function OrderCreate({
- onConfirm,
- onBack,
-}: Props): VNode {
+export default function OrderCreate({ onConfirm, onBack }: Props): VNode {
const { lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { state } = useSessionContext();
const detailsResult = useInstanceDetails();
const inventoryResult = useInstanceProducts();
- 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 +70,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 +80,7 @@ export default function OrderCreate({
return <NotFoundPageOrAdminCreate />;
}
case HttpStatusCode.Unauthorized: {
- return <LoginPage />
+ return <LoginPage />;
}
default: {
assertUnreachable(inventoryResult);
@@ -93,10 +95,11 @@ export default function OrderCreate({
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.PostOrderRequest) => {
- lib.instance.createOrder(state.token, request)
+ lib.instance
+ .createOrder(state.token, request)
.then((r) => {
if (r.type === "ok") {
- return onConfirm(r.body.order_id)
+ return onConfirm(r.body.order_id);
} else {
setNotif({
message: "could not create order",
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..4681f9943 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;
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..7b9691fdd 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 {
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..7866b9cd9 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
@@ -31,6 +31,7 @@ import {
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;
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..2fe3abaae 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;
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..08d42a8c9 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;
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..dc3e73850 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
@@ -38,6 +38,7 @@ 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;
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..336a336ed 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
@@ -145,7 +145,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
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 ?? ""),
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..4fe11bf5c 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
@@ -54,11 +54,10 @@ 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 instanceof TalerError) {
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..113cf5baa 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;
@@ -161,7 +162,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 ?? ""),
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..ad804831c 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
@@ -36,6 +36,7 @@ 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;
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/Create.stories.tsx
index 2fc0819bb..82038c918 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/products/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/Product/Create",
+ title: "Pages/TokenFamily/Create",
component: TestedComponent,
argTypes: {
onCreate: { action: "onCreate" },
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/deposit_confirmations/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/deposit_confirmations/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/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx
index 924e6d9b8..32c92cab0 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/webhooks/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,7 +16,7 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -24,38 +24,38 @@ 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";
-export type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
+export type Entity = MerchantBackend.TokenFamilies.TokenFamilyAddDetail;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
-
-export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
- const { createWebhook } = useWebhookAPI();
+export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
return (
- <>
+ <Fragment>
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => {
- return createWebhook(request)
+ onCreate={(request) => {
+ return lib.instance.createTokenFamily(state.token, request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
- message: i18n.str`could not inform template`,
+ message: i18n.str`could not create token family`,
type: "ERROR",
description: error.message,
});
});
}}
/>
- </>
+ </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..1af8a1192 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,25 @@
/**
*
- * @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 { MerchantBackend } from "../../../../declaration.js";
+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: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
) => Promise<void>;
onCreate: () => void;
selected?: boolean;
@@ -66,12 +58,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 +97,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: MerchantBackend.TokenFamilies.TokenFamilyPatchDetail,
) => Promise<void>;
- onDelete: (serial_id: Entity) => void;
+ onDelete: (tokenFamily: Entity) => void;
rowSelectionHandler: StateUpdater<string | undefined>;
}
@@ -123,55 +115,83 @@ function Table({
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>
+ <i18n.Translate>Slug</i18n.Translate>
</th>
<th>
- <i18n.Translate>Taxes</i18n.Translate>
+ <i18n.Translate>Name</i18n.Translate>
</th>
<th>
- <i18n.Translate>Sales</i18n.Translate>
+ <i18n.Translate>Valid After</i18n.Translate>
</th>
<th>
- <i18n.Translate>Stock</i18n.Translate>
+ <i18n.Translate>Valid Before</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 +203,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 +216,6 @@ function Table({
</div>
</td>
</tr>
- {rowSelection === i.id && (
- <tr key="form">
- <td colSpan={10}>
- </td>
- </tr>
- )}
</Fragment>
);
})}
@@ -211,16 +225,6 @@ function Table({
);
}
-interface FastProductUpdate {
- incoming: number;
- lost: number;
- price: string;
-}
-interface UpdatePrice {
- price: string;
-}
-
-
function EmptyTable(): VNode {
const { i18n } = useTranslationContext();
@@ -233,17 +237,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..58d071ffc
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx
@@ -0,0 +1,143 @@
+/*
+ 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 {
+ 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 {
+ useInstanceTokenFamilies,
+} from "../../../../hooks/tokenfamily.js";
+import { Notification } from "../../../../utils/types.js";
+import { CardTable } from "./Table.js";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../../../context/session.js";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { ConfirmModal } from "../../../../components/modal/index.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onCreate: () => void;
+ onSelect: (slug: string) => void;
+ onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+}
+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 {
+ await lib.instance.updateTokenFamily(state.token, slug, fam);
+ setNotif({
+ message: i18n.str`token family updated successfully`,
+ type: "SUCCESS",
+ });
+ } 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 {
+ await lib.instance.deleteTokenFamily(state.token, deleting.slug);
+ setNotif({
+ message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to delete token family`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the <b>&quot;{deleting.name}&quot;</b> token family (Slug:{" "}
+ <b>{deleting.slug}</b>), all issued tokens will become invalid.
+ </p>
+ <p class="warning">
+ Deleting a token family <b>cannot be undone</b>.
+ </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..5641d261b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx
@@ -0,0 +1,172 @@
+/*
+ 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 * as yup from "yup";
+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 { TokenFamilyUpdateSchema } from "../../../../schemas/index.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));
+ let errors: FormErrors<Entity> = {};
+
+ try {
+ TokenFamilyUpdateSchema.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 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);
+ }
+
+ 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">
+ Token Family: <b>{tokenFamily.name}</b>
+ </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`
+ : "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/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx
index 52f6c6c29..068235e14 100644
--- a/packages/auditor-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/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,7 +16,7 @@
/**
*
- * @author Sebastian Javier Marchano (sebasjm)
+ * @author Christian Blättler
*/
import {
@@ -28,69 +28,72 @@ 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";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import { useTokenFamilyDetails } from "../../../../hooks/tokenfamily.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId;
+type Entity = TalerMerchantApi.TokenFamilyUpdateRequest;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
- vid: string;
+ slug: string;
}
-export default function UpdateValidator({
- vid,
+export default function UpdateTokenFamily({
+ slug,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateOtpDevice } = useOtpDeviceAPI();
- const result = useOtpDeviceDetails(vid);
+ const result = useTokenFamilyDetails(slug);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib, state } = useSessionContext();
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);
+ 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
- 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
- }}
+ tokenFamily={family}
onBack={onBack}
onUpdate={(data) => {
- return updateOtpDevice(vid, data)
+ return lib.instance.updateTokenFamily(state.token, slug, data)
.then(onConfirm)
.catch((error) => {
setNotif({
- message: i18n.str`could not update template`,
+ message: i18n.str`could not update token family`,
type: "ERROR",
description: error.message,
});
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..927e36cf7 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,14 +73,14 @@ 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`}
+ placeholder={i18n.str`All accounts`}
tooltip={i18n.str`filter by account address`}
/>
</FormProvider>
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..5687d5e57 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;
@@ -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`}
onClick={() => onDelete(i)}
>
- Delete
+ <i18n.Translate>Delete</i18n.Translate>
</button>
) : undefined}
</td>
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..6a16446d8 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/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
index 6aca62582..bcd53ffd0 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
@@ -29,6 +29,7 @@ import {
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { WithId } from "../../../../declaration.js";
type Entity = TalerMerchantApi.WebhookPatchDetails & WithId;
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..6c0466dad 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
@@ -36,6 +36,7 @@ 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.WebhookPatchDetails & WithId;
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 0c4b9dd1a..ac23f7f09 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -45,6 +45,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
const next = s(value);
const v: Preferences = {
advanceOrderMode: next.advanceOrderMode ?? false,
+ advanceInstanceMode: next.advanceInstanceMode ?? false,
hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(),
hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(),
dateFormat: next.dateFormat ?? "ymd",
@@ -101,6 +102,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`}
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
index 693894ae0..77d23f49a 100644
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ b/packages/merchant-backoffice-ui/src/schemas/index.ts
@@ -23,6 +23,8 @@ 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";
+import { MerchantBackend } from "../declaration.js";
+// import { MerchantBackend } from "../declaration.js";
yup.setLocale({
mixed: {
@@ -222,3 +224,55 @@ export const NonInventoryProductSchema = yup.object().shape({
.required()
.test("amount", "the amount is not valid", currencyWithAmountIsValid),
});
+
+const timestampSchema = yup.object().shape({
+ t_s: yup.mixed().test(
+ 'is-timestamp',
+ 'Invalid timestamp',
+ value => typeof value === 'number' || value === 'never'
+ )
+}).required();
+
+const durationSchema = yup.object().shape({
+ d_us: yup.mixed().test(
+ 'is-duration',
+ 'Invalid duration',
+ value => typeof value === 'number' || value === 'forever'
+ )
+}).required();
+
+const tokenFamilyKindSchema = yup.mixed().oneOf<MerchantBackend.TokenFamilies.TokenFamilyKind>(["discount", "subscription"]).required();
+
+export const TokenFamilyCreateSchema = yup.object().shape({
+ slug: yup.string().ensure().required(),
+ name: yup.string().required(),
+ description: yup.string().required(),
+ // description_i18n: yup.lazy((obj) =>
+ // yup.object().shape(
+ // Object.keys(obj || {}).reduce((acc, key) => {
+ // acc[key] = yup.string().required();
+ // return acc;
+ // }, {})
+ // )
+ // ).optional(),
+ valid_after: timestampSchema.optional(),
+ valid_before: timestampSchema,
+ duration: durationSchema,
+ kind: tokenFamilyKindSchema,
+});
+
+export const TokenFamilyUpdateSchema = yup.object().shape({
+ name: yup.string().required(),
+ description: yup.string().required(),
+ // description_i18n: yup.lazy((obj) =>
+ // yup.object().shape(
+ // Object.keys(obj).reduce((acc, key) => {
+ // acc[key] = yup.string().required();
+ // return acc;
+ // }, {})
+ // )
+ // ),
+ valid_after: timestampSchema,
+ valid_before: timestampSchema,
+ duration: durationSchema,
+});
diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss
index 6c7346eb3..d3ff22997 100644
--- a/packages/merchant-backoffice-ui/src/scss/toggle.scss
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -50,9 +50,25 @@ $green: #56c080;
background: $green;
&:before {
+ left: 4px;
+ }
+ }
+ .toggle-checkbox:not(checked)+& {
+ background: $red;
+
+ &:before {
left: 30px;
}
}
+ .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/utils/table.ts b/packages/merchant-backoffice-ui/src/utils/table.ts
index 982b68e5e..5a94654f3 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";
+
/**
*
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 81d66125f..f57dbad40 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.12.2",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index a891cc7ba..64f750458 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,27 @@
+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/package.json b/packages/taler-harness/package.json
index bca870c8b..02bebcfce 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.12.2",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/bench2.ts b/packages/taler-harness/src/bench2.ts
index 90924caec..dc360ae62 100644
--- a/packages/taler-harness/src/bench2.ts
+++ b/packages/taler-harness/src/bench2.ts
@@ -30,7 +30,6 @@ import {
applyRunConfigDefaults,
CryptoDispatcher,
SynchronousCryptoWorkerFactoryPlain,
- Wallet,
} from "@gnu-taler/taler-wallet-core";
import {
checkReserve,
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 4fc462ddf..4cdde6b8d 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -805,6 +805,11 @@ 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 });
@@ -1468,6 +1473,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,
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index d194b0d36..2720be474 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -118,6 +118,8 @@ export interface EnvOptions {
skipWireFeeCreation?: boolean;
+ walletTestObservability?: boolean;
+
additionalExchangeConfig?(e: ExchangeService): void;
additionalMerchantConfig?(m: MerchantService): void;
additionalBankConfig?(b: BankService): void;
@@ -556,9 +558,12 @@ export async function createSimpleTestkudosEnvironmentV3(
),
});
- const { walletClient, walletService } = await createWalletDaemonWithClient(
- t,
- { name: "wallet", persistent: true },
+ const { walletClient, walletService } = await createWalletDaemonWithClient(t,
+ {
+ name: "wallet",
+ persistent: true,
+ emitObservabilityEvents: !!opts.walletTestObservability,
+ },
);
console.log("setup done!");
@@ -580,6 +585,7 @@ export interface CreateWalletArgs {
persistent?: boolean;
overrideDbPath?: string;
config?: PartialWalletRunConfig;
+ emitObservabilityEvents?: boolean;
}
export async function createWalletDaemonWithClient(
@@ -617,7 +623,7 @@ 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, {
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index 99b5502d8..864b7530c 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -302,8 +302,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(),
@@ -761,7 +759,7 @@ deploymentCli
"admin",
bankAdminPassword,
{
- scope: "write",
+ scope: "readwrite",
duration: {
d_us: 1000 * 1000 * 10, //10 secs
},
@@ -782,6 +780,7 @@ deploymentCli
*/
let accountPayto: PaytoString;
{
+ logger.info(`token: ${j2s(bankAdminToken)}`);
const resp = await bank.createAccount(bankAdminToken, {
name: name,
password: password,
@@ -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`);
@@ -1042,7 +1042,10 @@ deploymentCli
.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,7 +1062,7 @@ deploymentCli
const bankPassword = args.provisionMerchantInstance.bankPassword;
const accountPayto = args.provisionMerchantInstance.payto as PaytoString;
- const createResp = await api.createInstance(managementToken, {
+ const createResp = await managementApi.createInstance(managementToken, {
address: {},
auth: {
method: "token",
@@ -1086,18 +1089,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,6 +1154,7 @@ deploymentCli
logger.error(
`unable to provision bank account, HTTP response status ${resp.case}`,
);
+ logger.error(j2s(resp));
process.exit(2);
});
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-merchant-categories.ts b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts
new file mode 100644
index 000000000..5a7b690f7
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts
@@ -0,0 +1,162 @@
+/*
+ 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,
+ generateRandomPayto,
+ 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: [generateRandomPayto("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,
+ // FIXME: Don't hardcode
+ catgories: [myNewCategoryId],
+ },
+ });
+ 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, 2);
+
+ const prodFoo = posJson.products.find((x: any) => x.product_id = "foo");
+ t.assertTrue(!!prodFoo);
+ // Only default category
+ t.assertDeepEqual(prodFoo.categories, [0]);
+
+ const prodBar = posJson.products.find((x: any) => x.product_id = "bar");
+ 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-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-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
index 6e02071af..582f30299 100644
--- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -54,6 +54,8 @@ 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: {
@@ -122,6 +124,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({
@@ -165,7 +169,7 @@ export async function runRefundAutoTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: r1.transactionId,
txState: {
- major: TransactionMajorState.Pending,
+ major: TransactionMajorState.Finalizing,
minor: TransactionMinorState.AutoRefund,
},
});
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 65aa86f98..8714a3769 100644
--- a/packages/taler-harness/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -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);
@@ -239,7 +239,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-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-network-availability.ts b/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts
new file mode 100644
index 000000000..d97737e25
--- /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, generateRandomPayto } 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: generateRandomPayto("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-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-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
index 001081532..73585e206 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, exchange, merchant, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironmentV3(t, coinConfig, {});
console.log("setup done!");
@@ -114,7 +62,7 @@ export async function createMyEnvironment(
merchant,
walletClient,
walletService,
- bank,
+ bankClient,
exchangeBankAccount,
};
}
@@ -123,13 +71,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 +100,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 +125,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 +139,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 +183,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-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-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/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 4588310b1..eb71396e7 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -42,12 +42,14 @@ 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 { 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";
@@ -106,6 +108,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,6 +119,7 @@ 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";
@@ -229,11 +233,15 @@ const allTests: TestMainFunction[] = [
runWalletBlockedPayPeerPullTest,
runWalletExchangeUpdateTest,
runWalletRefreshErrorsTest,
+ runWalletNetworkAvailabilityTest,
runPeerPullLargeTest,
runPeerPushLargeTest,
runWithdrawalHandoverTest,
runWithdrawalAmountTest,
runWithdrawalFlexTest,
+ runExchangeMasterPubChangeTest,
+ runMerchantCategoriesTest,
+ runWithdrawalExternalTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 87e6a7cfa..c165489b3 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.12.2",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
index 9378d25e8..deb5d017b 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -107,6 +107,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,6 +170,14 @@ 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;
@@ -235,6 +247,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 = getDefaultHint(code);
+ }
+ return { code, hint, ...errBody };
+}
+
export class TalerError<T = any> extends Error {
errorDetail: TalerErrorDetail & T;
cause: Error | undefined;
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index e07b6c5fa..1e0f7e79c 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,
@@ -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));
}
}
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
index aa530570d..6a920749c 100644
--- a/packages/taler-util/src/http-client/challenger.ts
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -1,29 +1,31 @@
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,
+ opUnknownFailure
} from "../operation.js";
import {
AccessToken,
- codecForChallengeCreateResponse,
+ codecForChallengeInvalidPinResponse,
+ codecForChallengeResponse,
codecForChallengeSetupResponse,
+ codecForChallengeSolveResponse,
codecForChallengeStatus,
codecForChallengerAuthResponse,
codecForChallengerInfoResponse,
- codecForChallengerTermsOfServiceResponse,
- codecForInvalidPinResponse,
+ codecForChallengerTermsOfServiceResponse
} from "./types.js";
-import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js";
+import {
+ CacheEvictor,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
ResultByMethod<ChallengerHttpClient, prop>;
@@ -32,6 +34,7 @@ export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
export enum ChallengerCacheEviction {
CREATE_CHALLENGE,
+ SOLVE_CHALLENGE,
}
/**
@@ -45,7 +48,7 @@ export class ChallengerHttpClient {
readonly baseUrl: string,
httpClient?: HttpRequestLibrary,
cacheEvictor?: CacheEvictor<ChallengerCacheEviction>,
- ) {
+ ) {
this.httpLib = httpClient ?? createPlatformHttpLib();
this.cacheEvictor = cacheEvictor ?? nullEvictor;
}
@@ -116,7 +119,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",
});
@@ -158,13 +160,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,19 +194,16 @@ 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(),
- );
+ return opKnownAlternativeFailure(resp, HttpStatusCode.Forbidden, codecForChallengeInvalidPinResponse());
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotAcceptable:
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
index 892971fee..10afdc8eb 100644
--- a/packages/taler-util/src/http-client/merchant.ts
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -26,6 +26,7 @@ import {
codecForAccountAddResponse,
codecForAccountKycRedirects,
codecForAccountsSummaryResponse,
+ codecForBankAccountDetail,
codecForBankAccountEntry,
codecForClaimResponse,
codecForInstancesResponse,
@@ -707,7 +708,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:
@@ -882,9 +883,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
*/
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 0ef0bd65a..3816b1598 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -572,21 +572,22 @@ export const codecForAccountAddResponse =
export const codecForAccountsSummaryResponse =
(): Codec<TalerMerchantApi.AccountsSummaryResponse> =>
buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>()
- .property("accounts", codecForList(codecForBankAccountSummaryEntry()))
+ .property("accounts", codecForList(codecForBankAccountEntry()))
.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("active", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.BankAccountEntry");
+
+export const codecForBankAccountDetail =
+ (): Codec<TalerMerchantApi.BankAccountDetail> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountDetail>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
.property("salt", codecForString())
.property("credit_facade_url", codecOptional(codecForURL()))
.property("active", codecOptional(codecForBoolean()))
@@ -903,7 +904,6 @@ export const codecForTemplateDetails =
.property("template_description", codecForString())
.property("otp_id", codecOptional(codecForString()))
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -932,7 +932,6 @@ export const codecForWalletTemplateDetails =
(): Codec<TalerMerchantApi.WalletTemplateDetails> =>
buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
.property("template_contract", codecForTemplateContractDetails())
- .property("required_currency", codecOptional(codecForString()))
.property(
"editable_defaults",
codecOptional(codecForTemplateContractDetailsDefaults()),
@@ -1141,7 +1140,7 @@ export const codecForWithdrawalPublicInfo =
codecForConstString("confirmed"),
),
)
- .property("amount", codecForAmountString())
+ .property("amount", codecOptional(codecForAmountString()))
.property("username", codecForString())
.property("selected_reserve_pub", codecOptional(codecForString()))
.property(
@@ -1322,6 +1321,7 @@ export const codecForBankWithdrawalOperationStatus =
.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 =
@@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse =
.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 =
@@ -1578,25 +1586,46 @@ export const codecForChallengeSetupResponse =
export const codecForChallengeStatus =
(): Codec<ChallengerApi.ChallengeStatus> =>
buildCodecForObject<ChallengerApi.ChallengeStatus>()
- .property("restrictions", codecOptional(codecForMap(codecForAny())))
.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<ChallengerApi.ChallengeResponse> =>
+ buildCodecForUnion<ChallengerApi.ChallengeResponse>()
+ .discriminateOn("type")
+ .alternative("completed", codecForChallengeRedirect())
+ .alternative("created", codecForChallengeCreateResponse())
+ .build("ChallengerApi.ChallengeResponse");
+
export const codecForChallengeCreateResponse =
(): Codec<ChallengerApi.ChallengeCreateResponse> =>
buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
.property("attempts_left", codecForNumber())
+ .property("type", codecForConstString("created"))
.property("address", codecForAny())
.property("transmitted", codecForBoolean())
- .property("next_tx_time", codecForString())
+ .property("retransmission_time", codecForTimestamp)
.build("ChallengerApi.ChallengeCreateResponse");
-export const codecForInvalidPinResponse =
+export const codecForChallengeRedirect =
+ (): Codec<ChallengerApi.ChallengeRedirect> =>
+ buildCodecForObject<ChallengerApi.ChallengeRedirect>()
+ .property("type", codecForConstString("completed"))
+ .property("redirect_url", codecForString())
+ .build("ChallengerApi.ChallengeRedirect");
+
+export const codecForChallengeInvalidPinResponse =
(): Codec<ChallengerApi.InvalidPinResponse> =>
buildCodecForObject<ChallengerApi.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())
@@ -1604,6 +1633,14 @@ export const codecForInvalidPinResponse =
.property("no_challenge", codecForBoolean())
.build("ChallengerApi.InvalidPinResponse");
+export const codecForChallengeSolveResponse =
+ (): Codec<ChallengerApi.ChallengeSolveResponse> =>
+ buildCodecForUnion<ChallengerApi.ChallengeSolveResponse>()
+ .discriminateOn("type")
+ .alternative("completed", codecForChallengeRedirect())
+ .alternative("pending", codecForChallengeInvalidPinResponse())
+ .build("ChallengerApi.ChallengeSolveResponse");
+
export const codecForChallengerAuthResponse =
(): Codec<ChallengerApi.ChallengerAuthResponse> =>
buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
@@ -2210,13 +2247,6 @@ export namespace TalerCorebankApi {
// 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 {
@@ -2236,7 +2266,13 @@ export namespace TalerCorebankApi {
// Amount that will be withdrawn with this operation
// (raw amount without fee considerations).
- amount: AmountString;
+ 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;
@@ -4046,18 +4082,22 @@ export namespace TalerMerchantApi {
export interface AccountsSummaryResponse {
// List of accounts that are known for the instance.
- accounts: BankAccountSummaryEntry[];
+ accounts: BankAccountEntry[];
}
// TODO: missing in docs
- export interface BankAccountSummaryEntry {
+ 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 BankAccountEntry {
+ export interface BankAccountDetail {
// payto:// URI of the account.
payto_uri: PaytoString;
@@ -4752,17 +4792,6 @@ export namespace TalerMerchantApi {
// 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.
@@ -4814,17 +4843,6 @@ export namespace TalerMerchantApi {
// 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 {
@@ -4850,17 +4868,6 @@ export namespace TalerMerchantApi {
// 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 {
@@ -4879,17 +4886,6 @@ export namespace TalerMerchantApi {
// 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
@@ -5400,6 +5396,19 @@ export namespace ChallengerApi {
// 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 {
@@ -5414,16 +5423,6 @@ export namespace ChallengerApi {
}
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;
@@ -5435,9 +5434,37 @@ export namespace ChallengerApi {
// 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)
@@ -5453,10 +5480,14 @@ export namespace ChallengerApi {
// timestamp explaining when we would re-transmit the challenge the next
// time (at the earliest) if requested by the user
- next_tx_time: string;
+ 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;
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index d8cd36287..34be5a1d4 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -154,6 +154,7 @@ export async function readTalerErrorResponse(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"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",
);
@@ -193,6 +195,7 @@ export async function readUnexpectedResponseDetails(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"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",
);
@@ -242,6 +246,7 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Couldn't parse JSON format from response",
@@ -257,6 +262,7 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Response invalid",
@@ -282,6 +288,7 @@ export async function readResponseJsonOrErrorCode<T>(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Couldn't parse JSON format from response",
@@ -297,6 +304,7 @@ export async function readResponseJsonOrErrorCode<T>(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Response invalid",
@@ -376,6 +384,7 @@ export async function readSuccessResponseTextOrErrorCode<T>(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Couldn't parse JSON format from error response",
@@ -389,6 +398,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",
@@ -420,6 +430,7 @@ export async function checkSuccessResponseOrThrow(
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
+ response: await httpResponse.text(),
validationError: e.toString(),
},
"Couldn't parse JSON format from error response",
@@ -433,6 +444,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",
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 45a12c258..d27fd878d 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -181,14 +181,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 +236,9 @@ export class HttpLibImpl implements HttpRequestLibrary {
},
};
doCleanup();
+ if (SHOW_CURL_HTTP_REQUEST) {
+ console.log(`TALER_API_DEBUG: ${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..ea628676a 100644
--- a/packages/taler-util/src/http-impl.qtart.ts
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -72,7 +72,7 @@ export class HttpLibImpl implements HttpRequestLibrary {
async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
const method = (opt?.method ?? "GET").toUpperCase();
- logger.trace(`Requesting ${method} ${url}`);
+ logger.trace(`Requesting (Hi Marc) ${method} ${url}`);
const parsedUrl = new URL(url);
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
@@ -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,
@@ -172,6 +173,8 @@ export class HttpLibImpl implements HttpRequestLibrary {
throw e;
}
+ logger.trace(`got qtart http response, status ${res.status}`);
+
if (timeoutHandle != null) {
clearTimeout(timeoutHandle);
}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 9f99f2f5a..287e03584 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -50,6 +50,7 @@ export * from "./observability.js";
export * from "./operation.js";
export * from "./payto.js";
export * from "./promises.js";
+export * from "./qr.js";
export * from "./rfc3548.js";
export * from "./taler-crypto.js";
export * from "./taler-types.js";
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index a8a8c3299..49952295a 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -24,7 +24,11 @@
*/
import { AbsoluteTime } from "./time.js";
import { TransactionState } from "./transactions-types.js";
-import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
+import {
+ ExchangeEntryState,
+ TalerErrorDetail,
+ TransactionIdStr,
+} from "./wallet-types.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 =
@@ -217,6 +227,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..2d17238dc 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -146,7 +146,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,15 +182,51 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
}
}
+export async function succeedOrThrow<R, E>(
+ promise: Promise<OperationResult<R, E>>,
+): 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 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,
> = TT[p] extends (...args: any[]) => infer Ret
? Ret extends Promise<infer Result>
- ? Result extends OperationResult<any, any>
- ? Result
- : never
- : never //api always use Promises
+ ? Result extends OperationResult<any, any>
+ ? Result
+ : never
+ : never //api always use Promises
: never; //error cases just for functions
export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
@@ -195,4 +234,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..1b6907cbb 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, parsePaytoUri, stringifyPaytoUri } from "./payto.js";
test("basic payto parsing", (t) => {
const r1 = parsePaytoUri("https://example.com/");
@@ -29,3 +29,16 @@ 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);
+});
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 39c25cffd..2b55cdf64 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,7 +15,22 @@
*/
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,
+ bytesToString,
+ codecForAccessToken,
+ codecOptional,
+ hashTruncate32,
+ stringToBytes,
+} from "./index.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -151,15 +166,32 @@ export function addPaytoQueryParams(
params: { [name: string]: string },
): string {
const [acct, search] = s.slice(paytoPfx.length).split("?");
- const searchParams = new URLSearchParams(search || "");
- const keys = Object.keys(params);
- if (keys.length === 0) {
+ const paramList = !params ? [] : Object.entries(params);
+ if (paramList.length === 0) {
return paytoPfx + acct;
}
- for (const k of keys) {
- searchParams.set(k, params[k]);
- }
- 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 +203,15 @@ 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 {
+ const paytoUri = stringifyPaytoUri(p);
+ return bytesToString(hashTruncate32(stringToBytes(paytoUri + "\0")));
+}
+
/**
* Parse a valid payto:// uri into a PaytoUri object
* RFC 8905
@@ -205,7 +240,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 +330,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-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index c463d94a0..a1b6ccc77 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -2585,6 +2585,14 @@ 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 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).
@@ -2945,6 +2953,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 +4129,22 @@ 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,
+
+
+ /**
* 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-types.ts b/packages/taler-util/src/taler-types.ts
index 66f98ea9a..ac42ca278 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -723,6 +723,8 @@ export class ExchangeKeysJson {
currency: string;
+ currency_specification?: CurrencySpecification;
+
/**
* The exchange's master public key.
*/
@@ -1504,6 +1506,7 @@ 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)
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index b92366fb3..d80470dab 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -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..d3186d2f5 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString } from "./taler-types.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/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index a6ac5aec6..b4e2738ee 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -299,6 +299,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
*/
reserveIsReady: boolean;
+ /**
+ * Is the bank transfer for the withdrawal externally confirmed?
+ */
+ externalConfirmation?: boolean;
+
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
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/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index a7aa4f863..ec401f3f6 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -55,6 +55,7 @@ import {
} 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 {
@@ -557,11 +558,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 +573,22 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+/**
+ * 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 +646,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 +681,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 +1381,7 @@ export enum ExchangeTosStatus {
Pending = "pending",
Proposed = "proposed",
Accepted = "accepted",
+ MissingTos = "missing-tos",
}
export enum ExchangeEntryStatus {
@@ -1396,6 +1440,8 @@ export interface ExchangeListItem {
* to update the exchange info.
*/
lastUpdateErrorInfo?: OperationErrorInfo;
+
+ unavailableReason?: TalerErrorDetail;
}
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@@ -3371,3 +3417,49 @@ 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 {
+ wireTypes: string[];
+}
+
+export interface GetQrCodesForPaytoRequest {
+ paytoUri: string;
+}
+
+export const codecForGetQrCodesForPaytoRequest = () =>
+ buildCodecForObject<GetQrCodesForPaytoRequest>()
+ .property("paytoUri", codecForString())
+ .build("GetQrCodesForPaytoRequest");
+
+export interface GetQrCodesForPaytoResponse {
+ codes: QrCodeSpec[];
+}
+
+export type EmptyObject = Record<string, never>;
+
+export const codecForEmptyObject = (): Codec<EmptyObject> =>
+ buildCodecForObject<EmptyObject>()
+ .build("EmptyObject"); \ No newline at end of file
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index 5fa99e801..7ef8445dc 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,21 @@
+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..3430d525d 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.12.2",
"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..be74e464b 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,
@@ -722,6 +723,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 +743,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 +1380,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 +1705,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..273ad75f6 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.12.2",
"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/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index db6384c93..51316a21f 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -252,6 +252,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 +345,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 +357,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);
},
);
}
@@ -910,7 +922,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;
@@ -1131,17 +1146,127 @@ 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 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,
+ },
+ };
+ }
+ } 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,
+ depositFees: Amounts.parseOrThrow(r.customerDepositFees),
+ 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,105 +1280,7 @@ export async function selectPeerCoins(
],
},
async (tx): Promise<SelectPeerCoinsResult> => {
- 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 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,
- },
- };
- }
- } 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,
- depositFees: Amounts.parseOrThrow(r.customerDepositFees),
- 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,
- };
+ return selectPeerCoinsInTx(wex, tx, req);
},
);
}
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index a20278cf3..c37d0e21a 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -31,6 +31,8 @@ import {
ExchangeUpdateStatus,
Logger,
RefreshReason,
+ TalerError,
+ TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolTimestamp,
@@ -41,10 +43,10 @@ import {
checkDbInvariant,
checkLogicInvariant,
durationMul,
- j2s,
} from "@gnu-taler/taler-util";
import {
BackupProviderRecord,
+ CoinHistoryRecord,
CoinRecord,
DbPreciseTimestamp,
DepositGroupRecord,
@@ -62,6 +64,7 @@ import {
WithdrawalGroupRecord,
timestampPreciseToDb,
} from "./db.js";
+import { ReadyExchangeSummary } from "./exchanges.js";
import { createRefreshGroup } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
@@ -72,9 +75,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 +124,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,11 +150,16 @@ 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",
@@ -175,36 +186,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 +222,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 +247,7 @@ export async function spendCoins(
Amounts.currencyOf(csi.contributions[0]),
refreshCoinPubs,
csi.refreshReason,
- csi.allocationId,
+ csi.transactionId,
);
}
@@ -257,6 +268,9 @@ export enum TombstoneTag {
export function getExchangeTosStatusFromRecord(
exchange: ExchangeEntryRecord,
): ExchangeTosStatus {
+ if (exchange.tosCurrentEtag == null) {
+ return ExchangeTosStatus.MissingTos;
+ }
if (!exchange.tosAcceptedEtag) {
return ExchangeTosStatus.Proposed;
}
@@ -364,6 +378,7 @@ export enum TaskRunResultType {
Error = "error",
LongpollReturnedPending = "longpoll-returned-pending",
ScheduleLater = "schedule-later",
+ NetworkRequired = "network-required",
}
export type TaskRunResult =
@@ -372,7 +387,8 @@ export type TaskRunResult =
| TaskRunBackoffResult
| TaskRunProgressResult
| TaskRunLongpollReturnedPendingResult
- | TaskRunScheduleLaterResult;
+ | TaskRunScheduleLaterResult
+ | TaskRunNetworkRequiredResult;
export namespace TaskRunResult {
/**
@@ -419,6 +435,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 +467,10 @@ export interface TaskRunLongpollReturnedPendingResult {
type: TaskRunResultType.LongpollReturnedPending;
}
+export interface TaskRunNetworkRequiredResult {
+ type: TaskRunResultType.NetworkRequired;
+}
+
export interface TaskRunErrorResult {
type: TaskRunResultType.Error;
errorDetail: TalerErrorDetail;
@@ -818,3 +847,22 @@ export async function genericWaitForState(
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,
+ },
+ );
+ }
+}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 5c381eea7..138db157e 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -40,6 +40,7 @@ import {
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DenomLossEventType,
DenomSelectionState,
DenominationInfo,
@@ -51,6 +52,7 @@ import {
HashCodeString,
Logger,
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
@@ -61,6 +63,7 @@ import {
WireInfo,
WithdrawalExchangeAccountDetails,
codecForAny,
+ stringifyScopeInfo,
} from "@gnu-taler/taler-util";
import { DbRetryInfo, TaskIdentifiers } from "./common.js";
import {
@@ -151,7 +154,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 = 12;
declare const symDbProtocolTimestamp: unique symbol;
@@ -400,6 +403,8 @@ export interface ReserveBankInfo {
wireTypes: string[] | undefined;
currency: string | undefined;
+
+ externalConfirmation?: boolean;
}
/**
@@ -677,6 +682,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.
@@ -894,15 +901,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 +911,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 {
@@ -2339,6 +2373,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.
@@ -2373,6 +2424,12 @@ export const WalletStoresV1 = {
}),
},
}),
+ currencyInfo: describeStoreV2({
+ recordCodec: passthroughCodec<CurrencyInfoRecord>(),
+ storeName: "currencyInfo",
+ keyPath: "scopeInfoStr",
+ versionAdded: 12,
+ }),
globalCurrencyAuditors: describeStoreV2({
recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(),
storeName: "globalCurrencyAuditors",
@@ -2423,6 +2480,12 @@ export const WalletStoresV1 = {
}),
},
),
+ coinHistory: describeStoreV2({
+ storeName: "coinHistory",
+ recordCodec: passthroughCodec<CoinHistoryRecord>(),
+ keyPath: "coinPub",
+ versionAdded: 11,
+ }),
coins: describeStore(
"coins",
describeContents<CoinRecord>({
@@ -3325,3 +3388,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/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 2004c12cb..36ebf3974 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -72,7 +72,7 @@ import {
stringToBytes,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { selectPayCoins } from "./coinSelection.js";
+import { selectPayCoins, selectPayCoinsInTx } from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -518,12 +518,13 @@ async function refundDepositGroup(
const res = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"depositGroups",
"refreshGroups",
"refreshSessions",
- "coins",
- "denominations",
- "coinAvailability",
],
},
async (tx) => {
@@ -668,12 +669,20 @@ async function processDepositGroupPendingKyc(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
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,
+ });
+ },
+ );
+
if (
kycStatusRes.status === HttpStatusCode.Ok ||
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -979,43 +988,17 @@ async function processDepositGroupPendingDeposit(
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",
@@ -1029,6 +1012,45 @@ 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,
+ 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,
@@ -1041,7 +1063,7 @@ async function processDepositGroupPendingDeposit(
);
await tx.depositGroups.put(dg);
await spendCoins(wex, tx, {
- allocationId: transactionId,
+ transactionId,
coinPubs: dg.payCoinSelection.coinPubs,
contributions: dg.payCoinSelection.coinContributions.map((x) =>
Amounts.parseOrThrow(x),
@@ -1168,6 +1190,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) => {
@@ -1264,11 +1290,17 @@ 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}`);
+ 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: {
@@ -1606,20 +1638,21 @@ export async function createDepositGroup(
const newTxState = await wex.db.runReadWriteTx(
{
storeNames: [
- "depositGroups",
+ "coinAvailability",
+ "coinHistory",
"coins",
- "recoupGroups",
+ "contractTerms",
"denominations",
+ "depositGroups",
+ "recoupGroups",
"refreshGroups",
"refreshSessions",
- "coinAvailability",
- "contractTerms",
],
},
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),
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index adb696de0..04f4b27eb 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -28,10 +28,10 @@ import {
AgeRestriction,
Amount,
Amounts,
- AsyncFlag,
CancellationToken,
CoinRefreshRequest,
CoinStatus,
+ CurrencySpecification,
DeleteExchangeRequest,
DenomKeyType,
DenomLossEventType,
@@ -46,6 +46,7 @@ import {
ExchangeListItem,
ExchangeSignKeyJson,
ExchangeTosStatus,
+ ExchangeUpdateStatus,
ExchangeWireAccount,
ExchangesListResponse,
FeeDescription,
@@ -53,6 +54,7 @@ import {
GetExchangeResourcesResponse,
GetExchangeTosResult,
GlobalFees,
+ HttpStatusCode,
LibtoolVersion,
Logger,
NotificationType,
@@ -79,6 +81,7 @@ import {
WireInfo,
assertUnreachable,
checkDbInvariant,
+ checkLogicInvariant,
codecForExchangeKeysJson,
durationMul,
encodeCrock,
@@ -86,6 +89,7 @@ import {
hashDenomPub,
j2s,
makeErrorDetail,
+ makeTalerErrorDetail,
parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
@@ -93,6 +97,8 @@ import {
getExpiry,
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+ throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
@@ -103,6 +109,7 @@ import {
TransactionContext,
computeDbBackoff,
constructTaskIdentifier,
+ genericWaitForState,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
getExchangeState,
@@ -118,6 +125,7 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
ExchangeEntryRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
WalletStoresV1,
@@ -187,7 +195,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") || "";
@@ -321,7 +329,7 @@ async function makeExchangeListItem(
scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
}
- return {
+ const listItem: ExchangeListItem = {
exchangeBaseUrl: r.baseUrl,
masterPub: exchangeDetails?.masterPublicKey,
noFees: r.noFees ?? false,
@@ -342,6 +350,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 {
@@ -696,6 +712,7 @@ export interface ExchangeKeysDownloadResult {
globalFees: GlobalFees[];
accounts: ExchangeWireAccount[];
wireFees: { [methodName: string]: WireFeesJson[] };
+ currencySpecification?: CurrencySpecification;
}
/**
@@ -858,6 +875,42 @@ async function downloadExchangeKeysInfo(
globalFees: exchangeKeysJsonUnchecked.global_fees,
accounts: exchangeKeysJsonUnchecked.accounts,
wireFees: exchangeKeysJsonUnchecked.wire_fees,
+ currencySpecification: exchangeKeysJsonUnchecked.currency_specification,
+ };
+}
+
+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,
};
}
@@ -1008,132 +1061,6 @@ export interface ReadyExchangeSummary {
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;
- }
-}
-
/**
* Ensure that a fresh exchange entry exists for the given
* exchange base URL.
@@ -1186,39 +1113,131 @@ 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,
+ },
+ );
+ 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,
+ };
+
+ 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 +1305,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(
@@ -1367,7 +1390,6 @@ export async function updateExchangeFromUrlHandler(
AbsoluteTime.min(nextUpdateStamp, nextRefreshCheckStamp),
);
}
-
}
// When doing the auto-refresh check, we always update
@@ -1423,15 +1445,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 +1474,7 @@ export async function updateExchangeFromUrlHandler(
"recoupGroups",
"coinAvailability",
"denomLossEvents",
+ "currencyInfo",
],
},
async (tx) => {
@@ -1480,16 +1495,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 +1515,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 +1533,7 @@ export async function updateExchangeFromUrlHandler(
newExchangeState: getExchangeState(r),
};
}
+ delete r.unavailableReason;
r.updateRetryCounter = 0;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
@@ -1524,7 +1548,14 @@ export async function updateExchangeFromUrlHandler(
};
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 +1580,19 @@ export async function updateExchangeFromUrlHandler(
r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
+
+ if (keysInfo.currencySpecification) {
+ await WalletDbHelpers.insertCurrencyInfoUnlessExists(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",
@@ -1671,12 +1715,13 @@ export async function updateExchangeFromUrlHandler(
await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
"coins",
"denominations",
- "coinAvailability",
+ "exchanges",
"refreshGroups",
"refreshSessions",
- "exchanges",
],
},
async (tx) => {
@@ -2119,6 +2164,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,
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index ee154252f..efe6f6638 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -95,13 +95,18 @@ 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,
@@ -278,13 +283,14 @@ export class PayMerchantTransactionContext implements TransactionContext {
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
- "refreshGroups",
- "refreshSessions",
- "denominations",
"coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
"operationRetries",
+ "purchases",
+ "refreshGroups",
+ "refreshSessions",
],
},
async (tx) => {
@@ -472,33 +478,42 @@ 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,
@@ -533,13 +548,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 +561,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);
+ },
);
}
@@ -1046,7 +1062,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),
@@ -1117,6 +1133,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 +1166,6 @@ async function handleInsufficientFunds(
throw new TalerProtocolViolationError();
}
- const { contractData } = await expectProposalDownload(wex, proposal);
-
const prevPayCoins: PreviousPayCoins = [];
const payInfo = proposal.payInfo;
@@ -1162,53 +1178,19 @@ 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",
],
@@ -1222,6 +1204,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),
@@ -1230,11 +1252,7 @@ async function handleInsufficientFunds(
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
await tx.purchases.put(p);
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),
@@ -1444,6 +1462,7 @@ async function checkPaymentByProposalId(
const paid =
purchase.purchaseStatus === PurchaseStatus.Done ||
purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund ||
+ purchase.purchaseStatus === PurchaseStatus.FinalizingQueryingAutoRefund ||
purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund;
const download = await expectProposalDownload(wex, purchase);
return {
@@ -1936,44 +1955,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 +1969,15 @@ export async function confirmPay(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
+ "coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
+ "exchangeDetails",
+ "exchanges",
+ "purchases",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
],
},
async (tx) => {
@@ -2001,6 +1985,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:
@@ -2024,11 +2052,7 @@ export async function confirmPay(
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 +2060,6 @@ export async function confirmPay(
refreshReason: RefreshReason.PayMerchant,
});
}
-
break;
case PurchaseStatus.Done:
case PurchaseStatus.PendingPaying:
@@ -2086,6 +2109,10 @@ export async function processPurchase(
};
}
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
switch (purchase.purchaseStatus) {
case PurchaseStatus.PendingDownloadingProposal:
return processDownloadProposal(wex, proposalId);
@@ -2093,8 +2120,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:
@@ -2251,12 +2278,13 @@ async function processPurchasePay(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "purchases",
+ "coinAvailability",
+ "coinHistory",
"coins",
+ "denominations",
+ "purchases",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
],
},
async (tx) => {
@@ -2284,11 +2312,7 @@ async function processPurchasePay(
await tx.purchases.put(p);
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),
@@ -2851,14 +2875,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 ||
@@ -2998,12 +3033,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!
@@ -3389,16 +3430,17 @@ 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",
],
},
async (tx) => {
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index a1729ced7..636dd4156 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";
@@ -74,6 +78,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 +117,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 +152,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 +163,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..2ddebab29 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -59,6 +59,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -373,10 +374,16 @@ 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 resp = await wex.ws.runLongpollQueueing(
+ wex,
+ purseDepositUrl.hostname,
+ async () => {
+ return await wex.http.fetch(purseDepositUrl.href, {
+ timeout: { d_ms: 60000 },
+ cancellationToken: wex.cancellationToken,
+ });
+ },
+ );
const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullCredit,
pursePub: pullIni.pursePub,
@@ -487,12 +494,18 @@ async function longpollKycStatus(
`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 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,
+ });
+ },
+ );
if (
kycStatusRes.status === HttpStatusCode.Ok ||
// FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -766,6 +779,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) => {
@@ -933,6 +950,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 +1043,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,6 +1075,11 @@ 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();
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..4ad324359 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -237,11 +237,12 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
{
extraStores: [
"coinAvailability",
+ "coinAvailability",
+ "coinHistory",
+ "coins",
"denominations",
"refreshGroups",
"refreshSessions",
- "coins",
- "coinAvailability",
],
},
async (pi, tx) => {
@@ -475,13 +476,14 @@ async function processPeerPullDebitPendingDeposit(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
+ "coinAvailability",
+ "coinHistory",
"coins",
"denominations",
+ "exchanges",
+ "peerPullDebit",
"refreshGroups",
"refreshSessions",
- "peerPullDebit",
- "coinAvailability",
],
},
async (tx) => {
@@ -496,11 +498,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),
@@ -654,6 +652,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 +699,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, {
@@ -733,13 +738,14 @@ export async function confirmPeerPullDebit(
await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
+ "coinAvailability",
+ "coinHistory",
"coins",
"denominations",
+ "exchanges",
+ "peerPullDebit",
"refreshGroups",
"refreshSessions",
- "peerPullDebit",
- "coinAvailability",
],
},
async (tx) => {
@@ -752,11 +758,7 @@ export async function confirmPeerPullDebit(
}
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),
@@ -774,10 +776,6 @@ export async function confirmPeerPullDebit(
},
);
- const ctx = new PeerPullDebitTransactionContext(wex, peerPullDebitId);
-
- const transactionId = ctx.transactionId;
-
wex.ws.notify({
type: NotificationType.BalanceChange,
hintTransactionId: transactionId,
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..bd4adc0b6 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -61,6 +61,7 @@ import {
TombstoneTag,
TransactionContext,
constructTaskIdentifier,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
KycPendingInfo,
@@ -407,8 +408,6 @@ export async function preparePeerPushCredit(
const exchangeBaseUrl = uri.exchangeBaseUrl;
- await fetchFreshExchange(wex, exchangeBaseUrl);
-
const contractPriv = uri.contractPriv;
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
@@ -459,6 +458,12 @@ 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 transitionInfo = await wex.db.runReadWriteTx(
{ storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
@@ -532,12 +537,19 @@ async function longpollKycStatus(
`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 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",
+ 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
@@ -845,6 +857,10 @@ export async function processPeerPushCredit(
wex: WalletExecutionContext,
peerPushCreditId: string,
): Promise<TaskRunResult> {
+ if (!wex.ws.networkAvailable) {
+ return TaskRunResult.networkRequired();
+ }
+
let peerInc: PeerPushPaymentIncomingRecord | undefined;
let contractTerms: PeerContractTerms | undefined;
await wex.db.runReadWriteTx(
@@ -906,7 +922,7 @@ export async function confirmPeerPushCredit(
wex: WalletExecutionContext,
req: ConfirmPeerPushCreditRequest,
): Promise<AcceptPeerPushPaymentResponse> {
- let peerInc: PeerPushPaymentIncomingRecord | undefined;
+ // PeerPushPaymentIncomingRecord | undefined;
let peerPushCreditId: string;
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (!parsedTx) {
@@ -919,17 +935,18 @@ export async function confirmPeerPushCredit(
logger.trace(`confirming peer-push-credit ${peerPushCreditId}`);
- await wex.db.runReadWriteTx(
+ const peerInc = await wex.db.runReadWriteTx(
{ storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
- peerInc = await tx.peerPushCredit.get(peerPushCreditId);
- if (!peerInc) {
+ const rec = await tx.peerPushCredit.get(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);
+ return rec;
},
);
@@ -939,6 +956,9 @@ export async function confirmPeerPushCredit(
);
}
+ const exchange = await fetchFreshExchange(wex, peerInc.exchangeBaseUrl);
+ requireExchangeTosAcceptedOrThrow(exchange);
+
const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
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..f6bc605a0 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -51,7 +51,11 @@ import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
+import {
+ PreviousPayCoins,
+ selectPeerCoins,
+ selectPeerCoinsInTx,
+} from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -73,6 +77,7 @@ import {
import {
codecForExchangePurseStatus,
getTotalPeerPaymentCost,
+ getTotalPeerPaymentCostInTx,
queryCoinInfosForSelection,
} from "./pay-peer-common.js";
import { createRefreshGroup, waitRefreshFinal } from "./refresh.js";
@@ -511,14 +516,15 @@ async function processPeerPushDebitCreateReserve(
const transitionDone = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
- "contractTerms",
- "coins",
"coinAvailability",
+ "coinHistory",
+ "coins",
+ "contractTerms",
"denominations",
+ "exchanges",
+ "peerPushDebit",
"refreshGroups",
"refreshSessions",
- "peerPushDebit",
],
},
async (tx) => {
@@ -538,10 +544,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),
@@ -730,12 +733,13 @@ async function processPeerPushDebitAbortingDeletePurse(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"peerPushDebit",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
- "coins",
],
},
async (tx) => {
@@ -940,12 +944,18 @@ async function processPeerPushDebitReady(
`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 +981,13 @@ async function processPeerPushDebitReady(
const transitionInfo = await wex.db.runReadWriteTx(
{
storeNames: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
"peerPushDebit",
"refreshGroups",
"refreshSessions",
- "denominations",
- "coinAvailability",
- "coins",
],
},
async (tx) => {
@@ -1031,6 +1042,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) => {
@@ -1089,39 +1104,6 @@ export async function initiatePeerPushDebit(
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,20 +1112,49 @@ export async function initiatePeerPushDebit(
const contractEncNonce = encodeCrock(getRandomBytes(24));
- const transitionInfo = await wex.db.runReadWriteTx(
+ const res = await wex.db.runReadWriteTx(
{
storeNames: [
- "exchanges",
- "contractTerms",
- "coins",
"coinAvailability",
+ "coinHistory",
+ "coins",
+ "contractTerms",
"denominations",
+ "exchangeDetails",
+ "exchanges",
+ "peerPushDebit",
"refreshGroups",
"refreshSessions",
- "peerPushDebit",
],
},
async (tx) => {
+ const coinSelRes = await selectPeerCoinsInTx(wex, tx, {
+ 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;
+
+ const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins);
const ppi: PeerPushDebitRecord = {
amount: Amounts.stringify(instructedAmount),
contractPriv: contractKeyPair.priv,
@@ -1170,10 +1181,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),
@@ -1191,12 +1199,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);
+ notifyTransition(wex, transactionId, res.transitionInfo);
wex.ws.notify({
type: NotificationType.BalanceChange,
hintTransactionId: transactionId,
@@ -1208,7 +1219,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..a1710b1cd 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");
@@ -83,9 +84,17 @@ function requestToPromise(req: IDBRequest): Promise<any> {
resolve(req.result);
};
req.onerror = () => {
- console.error("error in DB request", req.error);
+ if (
+ req.error != null &&
+ "name" in req.error &&
+ req.error.name === "AbortError"
+ ) {
+ console.warn("DB request failed, transaction aborted");
+ } else {
+ console.error("error in DB request", req.error);
+ console.error("Request failed:", stack);
+ }
reject(req.error);
- console.error("Request failed:", stack);
};
});
}
@@ -565,34 +574,55 @@ function runTx<Arg, Res>(
arg: Arg,
f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
triggerContext: InternalTriggerContext,
+ cancellationToken: CancellationToken,
): Promise<Res> {
+ cancellationToken.throwIfCancelled();
+ // 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 = () => {
// 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 = () => {
+ if (cancellationToken.isCancelled) {
+ return;
+ }
logger.error("error in transaction");
logger.error(`${stack.stack}`);
};
tx.onabort = () => {
+ 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 +631,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));
@@ -614,6 +646,9 @@ function runTx<Arg, Res>(
.catch((e) => {
if (e == TransactionAbort) {
logger.trace("aborting transaction");
+ tx.abort();
+ } else if ("name" in e && e.name === "AbortError") {
+ console.warn("got AbortError, transaction was aborted");
} else {
transactionException = e;
console.error("Transaction failed:", e);
@@ -622,7 +657,7 @@ function runTx<Arg, Res>(
}
})
.catch((e) => {
- console.error("fatal: aborting transaction failed", e);
+ console.error("aborting failed:", safeStringifyException(e));
});
});
}
@@ -797,9 +832,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 +861,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 +878,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 +894,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;
@@ -919,7 +995,7 @@ 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 runTx(tx, writeContext, txf, triggerContext, this.cancellationToken);
}
async runAllStoresReadOnlyTx<T>(
@@ -946,7 +1022,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;
}
@@ -972,7 +1054,13 @@ 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;
}
@@ -998,7 +1086,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 = 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..43e9af625 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -199,24 +199,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);
@@ -300,6 +296,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) => {
@@ -396,12 +396,13 @@ export async function processRecoupGroup(
await wex.db.runReadWriteTx(
{
storeNames: [
- "recoupGroups",
"coinAvailability",
+ "coinHistory",
+ "coins",
"denominations",
+ "recoupGroups",
"refreshGroups",
"refreshSessions",
- "coins",
],
},
async (tx) => {
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 05c65f6b6..f782d2445 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -54,7 +54,6 @@ import {
makeErrorDetail,
NotificationType,
RefreshReason,
- TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
@@ -93,6 +92,7 @@ import {
import { CryptoApiStoppedError } from "./crypto/workers/crypto-dispatcher.js";
import {
CoinAvailabilityRecord,
+ CoinHistoryRecord,
CoinRecord,
CoinSourceType,
DenominationRecord,
@@ -1211,7 +1211,6 @@ async function refreshReveal(
coinEvHash: pc.coinEvHash,
maxAge: pc.maxAge,
ageCommitmentProof: pc.ageCommitmentProof,
- spendAllocation: undefined,
};
coins.push(coin);
@@ -1325,6 +1324,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 +1348,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(getErrorDetailFromException(err));
+ }
+ }
+
if (inShutdown) {
return TaskRunResult.finished();
}
@@ -1547,7 +1551,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 +1615,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,6 +1657,7 @@ export async function createRefreshGroup(
[
"denominations",
"coins",
+ "coinHistory",
"refreshGroups",
"refreshSessions",
"coinAvailability",
@@ -1796,6 +1815,7 @@ export async function forceRefresh(
"refreshSessions",
"denominations",
"coins",
+ "coinHistory",
],
},
async (tx) => {
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index 470f45aff..434ee8635 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -145,6 +145,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 +196,9 @@ export class TaskSchedulerImpl implements TaskScheduler {
}
}
+ /**
+ * @see TaskScheduler.ensureRunning
+ */
async ensureRunning(): Promise<void> {
if (this.isRunning) {
return;
@@ -261,7 +272,7 @@ 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);
@@ -276,9 +287,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 = {
@@ -464,6 +476,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);
}
@@ -613,6 +633,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);
@@ -843,91 +872,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.
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
index 899c4a8b2..6435595cb 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,
@@ -122,6 +125,9 @@ export async function withdrawTestBalance(
amount,
);
+ await fetchFreshExchange(wex, req.exchangeBaseUrl);
+ await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl);
+
const acceptResp = await acceptWithdrawalFromUri(wex, {
talerWithdrawUri: wresp.taler_withdraw_uri,
selectedExchange: exchangeBaseUrl,
@@ -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:
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 7782d09ba..0649f9ce2 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -99,7 +99,7 @@ import {
computePayMerchantTransactionActions,
computePayMerchantTransactionState,
computeRefundTransactionState,
- expectProposalDownload,
+ expectProposalDownloadInTx,
extractContractData,
PayMerchantTransactionContext,
RefundTransactionContext,
@@ -306,7 +306,7 @@ export async function getTransactionById(
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) throw Error("not found");
- const download = await expectProposalDownload(wex, purchase, tx);
+ const download = await expectProposalDownloadInTx(wex, tx, purchase);
const contractData = download.contractData;
const payOpId = TaskIdentifiers.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId);
@@ -744,7 +744,10 @@ function buildTransactionForBankIntegratedWithdraw(
? undefined
: Amounts.currencyOf(wg.instructedAmount);
const currency = wg.wgInfo.bankInfo.currency ?? instructedCurrency;
- checkDbInvariant(currency !== undefined, "wg uninitialized (missing currency)");
+ checkDbInvariant(
+ currency !== undefined,
+ "wg uninitialized (missing currency)",
+ );
const txState = computeWithdrawalTransactionStatus(wg);
const zero = Amounts.stringify(Amounts.zeroOfCurrency(currency));
@@ -765,7 +768,10 @@ function buildTransactionForBankIntegratedWithdraw(
confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false,
exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts,
reservePub: wg.reservePub,
- bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl,
+ bankConfirmationUrl: wg.wgInfo.bankInfo.externalConfirmation
+ ? undefined
+ : wg.wgInfo.bankInfo.confirmUrl,
+ externalConfirmation: wg.wgInfo.bankInfo.externalConfirmation,
reserveIsReady:
wg.status === WithdrawalGroupStatus.Done ||
wg.status === WithdrawalGroupStatus.PendingReady,
@@ -1812,6 +1818,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,
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..12abb6469 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -59,6 +59,7 @@ import {
DeleteExchangeRequest,
DeleteStoredBackupRequest,
DeleteTransactionRequest,
+ EmptyObject,
ExchangeDetailedResponse,
ExchangesListResponse,
ExchangesShortListResponse,
@@ -71,6 +72,8 @@ import {
GetContractTermsDetailsRequest,
GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
+ GetDepositWireTypesForCurrencyRequest,
+ GetDepositWireTypesForCurrencyResponse,
GetExchangeEntryByUrlRequest,
GetExchangeEntryByUrlResponse,
GetExchangeResourcesRequest,
@@ -79,6 +82,8 @@ import {
GetExchangeTosResult,
GetPlanForOperationRequest,
GetPlanForOperationResponse,
+ GetQrCodesForPaytoRequest,
+ GetQrCodesForPaytoResponse,
GetWithdrawalDetailsForAmountRequest,
GetWithdrawalDetailsForUriRequest,
HintNetworkAvailabilityRequest,
@@ -90,6 +95,7 @@ import {
InitiatePeerPushDebitRequest,
InitiatePeerPushDebitResponse,
IntegrationTestArgs,
+ IntegrationTestV2Args,
KnownBankAccounts,
ListAssociatedRefreshesRequest,
ListAssociatedRefreshesResponse,
@@ -127,8 +133,6 @@ import {
TestPayResult,
TestingGetDenomStatsRequest,
TestingGetDenomStatsResponse,
- TestingListTasksForTransactionRequest,
- TestingListTasksForTransactionsResponse,
TestingSetTimetravelRequest,
TestingWaitTransactionRequest,
Transaction,
@@ -190,18 +194,17 @@ export enum WalletApiOperation {
GetBalances = "getBalances",
GetBalanceDetail = "getBalanceDetail",
GetPlanForOperation = "getPlanForOperation",
- ConvertDepositAmount = "ConvertDepositAmount",
- GetMaxDepositAmount = "GetMaxDepositAmount",
+ ConvertDepositAmount = "convertDepositAmount",
+ GetMaxDepositAmount = "getMaxDepositAmount",
ConvertPeerPushAmount = "ConvertPeerPushAmount",
- GetMaxPeerPushAmount = "GetMaxPeerPushAmount",
- ConvertWithdrawalAmount = "ConvertWithdrawalAmount",
+ GetMaxPeerPushAmount = "getMaxPeerPushAmount",
+ ConvertWithdrawalAmount = "convertWithdrawalAmount",
GetUserAttentionRequests = "getUserAttentionRequests",
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
- GetPendingOperations = "getPendingOperations",
GetActiveTasks = "getActiveTasks",
SetExchangeTosAccepted = "setExchangeTosAccepted",
- SetExchangeTosForgotten = "SetExchangeTosForgotten",
+ SetExchangeTosForgotten = "setExchangeTosForgotten",
StartRefundQueryForUri = "startRefundQueryForUri",
StartRefundQuery = "startRefundQuery",
PrepareBankIntegratedWithdrawal = "prepareBankIntegratedWithdrawal",
@@ -266,13 +269,13 @@ export enum WalletApiOperation {
Shutdown = "shutdown",
HintNetworkAvailability = "hintNetworkAvailability",
CanonicalizeBaseUrl = "canonicalizeBaseUrl",
+ GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency",
+ GetQrCodesForPayto = "getQrCodesForPayto",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
TestingWaitTransactionState = "testingWaitTransactionState",
TestingWaitTasksDone = "testingWaitTasksDone",
TestingSetTimetravel = "testingSetTimetravel",
- TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop",
- TestingListTaskForTransaction = "testingListTasksForTransaction",
TestingGetDenomStats = "testingGetDenomStats",
TestingPing = "testingPing",
TestingGetReserveHistory = "testingGetReserveHistory",
@@ -281,8 +284,6 @@ export enum WalletApiOperation {
// group: Initialization
-type EmptyObject = Record<string, never>;
-
/**
* Initialize wallet-core.
*
@@ -729,6 +730,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 = {
@@ -986,6 +997,12 @@ export type CanonicalizeBaseUrlOp = {
response: CanonicalizeBaseUrlResponse;
};
+export type GetQrCodesForPaytoOp = {
+ op: WalletApiOperation.GetQrCodesForPayto;
+ request: GetQrCodesForPaytoRequest;
+ response: GetQrCodesForPaytoResponse;
+};
+
// group: Database Management
/**
@@ -1051,7 +1068,7 @@ export type RunIntegrationTestOp = {
*/
export type RunIntegrationTestV2Op = {
op: WalletApiOperation.RunIntegrationTestV2;
- request: IntegrationTestArgs;
+ request: IntegrationTestV2Args;
response: EmptyObject;
};
@@ -1120,17 +1137,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 +1162,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 = {
@@ -1279,7 +1276,6 @@ export type WalletOperations = {
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
[WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;
- [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp;
[WalletApiOperation.GetActiveTasks]: GetActiveTasksOp;
[WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests;
[WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount;
@@ -1348,7 +1344,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 +1353,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 +1362,8 @@ export type WalletOperations = {
[WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
[WalletApiOperation.TestingResetAllRetries]: TestingResetAllRetriesOp;
[WalletApiOperation.HintNetworkAvailability]: HintNetworkAvailabilityOp;
+ [WalletApiOperation.GetDepositWireTypesForCurrency]: GetDepositWireTypesForCurrencyOp;
+ [WalletApiOperation.GetQrCodesForPayto]: GetQrCodesForPaytoOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index f1d53b7d5..5b3b4da29 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -24,25 +24,56 @@
*/
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,
+ GetContractTermsDetailsRequest,
+ GetCurrencySpecificationRequest,
GetCurrencySpecificationResponse,
+ GetDepositWireTypesForCurrencyRequest,
+ GetDepositWireTypesForCurrencyResponse,
+ GetExchangeTosRequest,
+ GetExchangeTosResult,
+ GetQrCodesForPaytoRequest,
+ GetQrCodesForPaytoResponse,
+ HintNetworkAvailabilityRequest,
+ InitRequest,
InitResponse,
+ IntegrationTestArgs,
+ IntegrationTestV2Args,
KnownBankAccounts,
KnownBankAccountsInfo,
+ ListExchangesForScopedCurrencyRequest,
ListGlobalCurrencyAuditorsResponse,
ListGlobalCurrencyExchangesResponse,
Logger,
@@ -55,21 +86,35 @@ import {
PrepareWithdrawExchangeRequest,
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
+ RemoveGlobalCurrencyAuditorRequest,
+ RemoveGlobalCurrencyExchangeRequest,
+ SharePaymentRequest,
+ SharePaymentResult,
+ StartRefundQueryRequest,
StoredBackupList,
+ SuspendTransactionRequest,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerProtocolTimestamp,
TalerUriAction,
+ TestingGetDenomStatsRequest,
TestingGetDenomStatsResponse,
- TestingListTasksForTransactionsResponse,
+ TestingGetReserveHistoryRequest,
+ TestingSetTimetravelRequest,
TestingWaitTransactionRequest,
TimerAPI,
TimerGroup,
TransactionType,
+ TransactionsResponse,
+ UpdateExchangeEntryRequest,
+ ValidateIbanRequest,
ValidateIbanResponse,
+ WalletContractData,
WalletCoreVersion,
WalletNotification,
WalletRunConfig,
+ WithdrawTestBalanceRequest,
canonicalizeBaseUrl,
checkDbInvariant,
codecForAbortTransaction,
@@ -95,6 +140,7 @@ import {
codecForDeleteExchangeRequest,
codecForDeleteStoredBackupRequest,
codecForDeleteTransactionRequest,
+ codecForEmptyObject,
codecForFailTransactionRequest,
codecForForceRefreshRequest,
codecForForgetKnownBankAccounts,
@@ -102,9 +148,11 @@ import {
codecForGetBalanceDetailRequest,
codecForGetContractTermsDetails,
codecForGetCurrencyInfoRequest,
+ codecForGetDepositWireTypesForCurrencyRequest,
codecForGetExchangeEntryByUrlRequest,
codecForGetExchangeResourcesRequest,
codecForGetExchangeTosRequest,
+ codecForGetQrCodesForPaytoRequest,
codecForGetWithdrawalDetailsForAmountRequest,
codecForGetWithdrawalDetailsForUri,
codecForHintNetworkAvailabilityRequest,
@@ -137,7 +185,6 @@ import {
codecForTestPayArgs,
codecForTestingGetDenomStatsRequest,
codecForTestingGetReserveHistoryRequest,
- codecForTestingListTasksForTransactionRequest,
codecForTestingSetTimetravelRequest,
codecForTransactionByIdRequest,
codecForTransactionsRequest,
@@ -147,13 +194,13 @@ import {
codecForValidateIbanRequest,
codecForWithdrawTestBalance,
getErrorDetailFromException,
+ getQrCodesForPayto,
j2s,
openPromise,
parsePaytoUri,
parseTalerUri,
performanceNow,
safeStringifyException,
- sampleWalletCoreTransactions,
setDangerousTimetravel,
validateIban,
} from "@gnu-taler/taler-util";
@@ -167,6 +214,7 @@ import {
markAttentionRequestAsRead,
} from "./attention.js";
import {
+ RunBackupCycleRequest,
addBackupProvider,
codecForAddBackupProviderRequest,
codecForRemoveBackupProvider,
@@ -188,6 +236,7 @@ import {
CoinSourceType,
ConfigRecordKey,
DenominationRecord,
+ WalletDbHelpers,
WalletDbReadOnlyTransaction,
WalletStoresV1,
clearDatabase,
@@ -214,6 +263,7 @@ import {
getExchangeDetailedInfo,
getExchangeResources,
getExchangeTos,
+ getExchangeWireDetailsInTx,
listExchanges,
lookupExchangeByUri,
} from "./exchanges.js";
@@ -267,7 +317,6 @@ import {
TaskSchedulerImpl,
convertTaskToTransactionId,
getActiveTaskIds,
- listTaskForTransactionId,
} from "./shepherd.js";
import {
runIntegrationTest,
@@ -288,6 +337,7 @@ import {
getTransactions,
getWithdrawalTransactionByUri,
parseTransactionIdentifier,
+ restartAll as restartAllRunningTasks,
resumeTransaction,
retryAll,
retryTransaction,
@@ -295,7 +345,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,6 +353,7 @@ import {
import {
WalletApiOperation,
WalletCoreApiClient,
+ WalletCoreRequestType,
WalletCoreResponseType,
} from "./wallet-api-types.js";
import {
@@ -479,7 +529,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 +564,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 +595,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 : [],
});
}
},
@@ -677,18 +726,728 @@ 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 {};
+}
+
+async function handleSharePayment(
+ wex: WalletExecutionContext,
+ 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)}`);
+ }
+
+ // 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: 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 {};
+}
+
+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++;
+ }
+ }
+ });
+ 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 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);
+ }
+ 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 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) => {
+ 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 handleAddBackupProvider(
+ wex: WalletExecutionContext,
+ req: RunBackupCycleRequest,
+): Promise<EmptyObject> {
+ await runBackupCycle(wex, req);
+ return {};
+}
+
+async function handleHintNetworkAvailability(
+ wex: WalletExecutionContext,
+ req: HintNetworkAvailabilityRequest,
+): Promise<EmptyObject> {
+ 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();
+ 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 (!usable) {
+ break;
+ }
+ const parsedPayto = parsePaytoUri(acc.payto_uri);
+ if (!parsedPayto) {
+ continue;
+ }
+ wtSet.add(parsedPayto.targetType);
+ }
+ }
+ },
+ );
+ return {
+ wireTypes: [...wtSet],
+ };
+}
+
+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,
+ });
+ }
+ },
+ );
+ 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,
+ exchangeMasterPub: req.exchangeMasterPub,
+ });
+ },
+ );
+ 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;
+ }
+ 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;
+ }
+ 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;
+ }
+ 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 acceptWithdrawalFromUri(wex, {
+ selectedExchange: req.exchangeBaseUrl,
+ talerWithdrawUri: req.talerWithdrawUri,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ amount: req.amount,
+ });
+}
+
+interface HandlerWithValidator<Tag extends WalletApiOperation> {
+ codec: Codec<WalletCoreRequestType<Tag>>;
+ handler: (
+ wex: WalletExecutionContext,
+ req: WalletCoreRequestType<Tag>,
+ ) => Promise<WalletCoreResponseType<Tag>>;
}
+// @ts-ignore
+const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
+ [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,
+ },
+};
+
/**
* Implementation of the "wallet-core" API.
*/
@@ -707,105 +1466,40 @@ async function dispatchRequestInternal(
// definitions we already have?
switch (operation) {
case WalletApiOperation.CreateStoredBackup:
- return createStoredBackup(wex);
+ return await handleCreateStoredBackup(wex, {});
case WalletApiOperation.DeleteStoredBackup: {
const req = codecForDeleteStoredBackupRequest().decode(payload);
- await deleteStoredBackup(wex, req);
- return {};
+ return await handleDeleteStoredBackup(wex, req);
}
case WalletApiOperation.ListStoredBackups:
return listStoredBackups(wex);
case WalletApiOperation.RecoverStoredBackup: {
const req = codecForRecoverStoredBackupRequest().decode(payload);
- await recoverStoredBackup(wex, req);
- return {};
+ return await handleRecoverStoredBackup(wex, req);
}
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));
-
- 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 (req.config?.lazyTaskLoop) {
- logger.trace("lazily starting task loop");
- } else {
- await wex.taskScheduler.ensureRunning();
- }
-
- wex.ws.initCalled = true;
- return resp;
+ return await handleSetWalletRunConfig(wex, req);
}
case WalletApiOperation.WithdrawTestkudos: {
- await withdrawTestBalance(wex, {
- amount: "TESTKUDOS:10" as AmountString,
- corebankApiBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
- });
- return {
- versionInfo: getVersion(wex),
- };
+ return await handleWithdrawTestkudos(wex);
}
case WalletApiOperation.WithdrawTestBalance: {
const req = codecForWithdrawTestBalance().decode(payload);
- await withdrawTestBalance(wex, req);
- return {};
- }
- case WalletApiOperation.TestingListTaskForTransaction: {
- const req =
- codecForTestingListTasksForTransactionRequest().decode(payload);
- return {
- taskIdList: listTaskForTransactionId(req.transactionId),
- } satisfies TestingListTasksForTransactionsResponse;
+ return await handleWithdrawTestBalance(wex, req);
}
case WalletApiOperation.RunIntegrationTest: {
const req = codecForIntegrationTestArgs().decode(payload);
- await runIntegrationTest(wex, req);
- return {};
+ return await handleRunIntegrationTest(wex, req);
}
case WalletApiOperation.RunIntegrationTestV2: {
const req = codecForIntegrationTestV2Args().decode(payload);
- await runIntegrationTest2(wex, req);
- return {};
+ return await handleRunIntegrationTestV2(wex, req);
}
case WalletApiOperation.ValidateIban: {
const req = codecForValidateIbanRequest().decode(payload);
- const valRes = validateIban(req.iban);
- const resp: ValidateIbanResponse = {
- valid: valRes.type === "valid",
- };
- return resp;
+ return handleValidateIban(wex, req);
}
case WalletApiOperation.TestPay: {
const req = codecForTestPayArgs().decode(payload);
@@ -825,45 +1519,18 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.AddExchange: {
const req = codecForAddExchangeRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {});
- return {};
+ return await handleAddExchange(wex, req);
}
case WalletApiOperation.TestingPing: {
return {};
}
case WalletApiOperation.UpdateExchangeEntry: {
const req = codecForUpdateExchangeEntryRequest().decode(payload);
- await fetchFreshExchange(wex, req.exchangeBaseUrl, {
- forceUpdate: !!req.force,
- });
- return {};
+ return await handleUpdateExchangeEntry(wex, req);
}
case WalletApiOperation.TestingGetDenomStats: {
const req = codecForTestingGetDenomStatsRequest().decode(payload);
- 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++;
- }
- }
- },
- );
- return denomStats;
+ return handleTestingGetDenomStats(wex, req);
}
case WalletApiOperation.ListExchanges: {
return await listExchanges(wex);
@@ -875,20 +1542,7 @@ async function dispatchRequestInternal(
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,
- });
- }
- }
- return result;
+ return await handleListExchangesForScopedCurrency(wex, req);
}
case WalletApiOperation.GetExchangeDetailedInfo: {
const req = codecForAddExchangeRequest().decode(payload);
@@ -900,13 +1554,11 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.AddKnownBankAccounts: {
const req = codecForAddKnownBankAccounts().decode(payload);
- await addKnownBankAccounts(wex, req.payto, req.alias, req.currency);
- return {};
+ return await handleAddKnownBankAccount(wex, req);
}
case WalletApiOperation.ForgetKnownBankAccounts: {
const req = codecForForgetKnownBankAccounts().decode(payload);
- await forgetKnownBankAccounts(wex, req.payto);
- return {};
+ return await handleForgetKnownBankAccounts(wex, req);
}
case WalletApiOperation.GetWithdrawalDetailsForUri: {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
@@ -914,48 +1566,16 @@ async function dispatchRequestInternal(
}
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");
- }
- 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;
+ return await handleTestingGetReserveHistory(wex, req);
}
case WalletApiOperation.AcceptManualWithdrawal: {
const req = codecForAcceptManualWithdrawalRequest().decode(payload);
- const res = await createManualWithdrawal(wex, {
- amount: Amounts.parseOrThrow(req.amount),
- exchangeBaseUrl: req.exchangeBaseUrl,
- restrictAge: req.restrictAge,
- forceReservePriv: req.forceReservePriv,
- });
- return res;
+ return await handleAcceptManualWithdrawal(wex, req);
}
case WalletApiOperation.GetWithdrawalDetailsForAmount: {
const req =
codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
- const resp = await getWithdrawalDetailsForAmount(wex, cts, req);
- return resp;
+ return await getWithdrawalDetailsForAmount(wex, cts, req);
}
case WalletApiOperation.GetBalances: {
return await getBalances(wex);
@@ -976,12 +1596,6 @@ async function dispatchRequestInternal(
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);
@@ -995,13 +1609,7 @@ async function dispatchRequestInternal(
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,
- });
+ return handleAcceptBankIntegratedWithdrawal(wex, req);
}
case WalletApiOperation.ConfirmWithdrawal: {
const req = codecForConfirmWithdrawalRequestRequest().decode(payload);
@@ -1014,39 +1622,22 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
- return getExchangeTos(
- wex,
- req.exchangeBaseUrl,
- req.acceptedFormat,
- req.acceptLanguage,
- );
+ return await handleGetExchangeTos(wex, req);
}
case WalletApiOperation.GetContractTermsDetails: {
const req = codecForGetContractTermsDetails().decode(payload);
- 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);
- }
- throw Error("transactionId is not a payment transaction");
- }
- throw Error("transactionId missing");
+ return handleGetContractTermsDetails(wex, req);
}
case WalletApiOperation.RetryPendingNow: {
- logger.error("retryPendingNow currently not implemented");
- return {};
+ return handleRetryPendingNow(wex);
}
case WalletApiOperation.SharePayment: {
const req = codecForSharePaymentRequest().decode(payload);
- return await sharePayment(wex, req.merchantBaseUrl, req.orderId);
+ return await handleSharePayment(wex, req);
}
case WalletApiOperation.PrepareWithdrawExchange: {
const req = codecForPrepareWithdrawExchangeRequest().decode(payload);
- return handlePrepareWithdrawExchange(wex, req);
+ return await handlePrepareWithdrawExchange(wex, req);
}
case WalletApiOperation.CheckPayForTemplate: {
const req = codecForCheckPayTemplateRequest().decode(payload);
@@ -1060,74 +1651,28 @@ async function dispatchRequestInternal(
const req = codecForPreparePayTemplateRequest().decode(payload);
return preparePayForTemplate(wex, req);
}
+ case WalletApiOperation.GetQrCodesForPayto: {
+ const req = codecForGetQrCodesForPaytoRequest().decode(payload);
+ return handleGetQrCodesForPayto(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");
- }
- return await confirmPay(wex, transactionId, req.sessionId);
+ return handleConfirmPay(wex, req);
}
case WalletApiOperation.AbortTransaction: {
const req = codecForAbortTransaction().decode(payload);
- await abortTransaction(wex, req.transactionId);
- return {};
+ return handleAbortTransaction(wex, req);
}
case WalletApiOperation.SuspendTransaction: {
const req = codecForSuspendTransaction().decode(payload);
- await suspendTransaction(wex, req.transactionId);
- return {};
+ return handleSuspendTransaction(wex, req);
}
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 };
+ return await handleGetActiveTasks(wex, {});
}
case WalletApiOperation.FailTransaction: {
const req = codecForFailTransactionRequest().decode(payload);
- await failTransaction(wex, req.transactionId);
- return {};
+ return await handleFailTransaction(wex, req);
}
case WalletApiOperation.ResumeTransaction: {
const req = codecForResumeTransaction().decode(payload);
@@ -1143,7 +1688,8 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.TestingGetSampleTransactions:
- return { transactions: sampleWalletCoreTransactions };
+ const req = codecForEmptyObject().decode(payload);
+ return handleTestingGetSampleTransactions(wex, req);
case WalletApiOperation.ForceRefresh: {
const req = codecForForceRefreshRequest().decode(payload);
return await forceRefresh(wex, req);
@@ -1154,15 +1700,7 @@ async function dispatchRequestInternal(
}
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 {};
+ return handleStartRefundQuery(wex, req);
}
case WalletApiOperation.AddBackupProvider: {
const req = codecForAddBackupProviderRequest().decode(payload);
@@ -1170,8 +1708,7 @@ async function dispatchRequestInternal(
}
case WalletApiOperation.RunBackupCycle: {
const req = codecForRunBackupCycle().decode(payload);
- await runBackupCycle(wex, req);
- return {};
+ return handleAddBackupProvider(wex, req);
}
case WalletApiOperation.RemoveBackupProvider: {
const req = codecForRemoveBackupProvider().decode(payload);
@@ -1188,48 +1725,8 @@ async function dispatchRequestInternal(
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;
+ return handleGetCurrencySpecification(wex, req);
}
case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload);
@@ -1238,13 +1735,7 @@ async function dispatchRequestInternal(
}
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 {};
+ return await handleHintNetworkAvailability(wex, req);
}
case WalletApiOperation.ConvertDepositAmount: {
const req = codecForConvertAmountRequest.decode(payload);
@@ -1313,117 +1804,30 @@ async function dispatchRequestInternal(
const dbDump = await exportDb(wex.ws.idb);
return dbDump;
}
+ case WalletApiOperation.GetDepositWireTypesForCurrency: {
+ const req =
+ codecForGetDepositWireTypesForCurrencyRequest().decode(payload);
+ return handleGetDepositWireTypesForCurrency(wex, req);
+ }
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;
+ const req = codecForEmptyObject().decode(payload);
+ return await handleListGlobalCurrencyExchanges(wex, req);
}
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;
+ const req = codecForEmptyObject().decode(payload);
+ return await handleListGlobalCurrencyAuditors(wex, req);
}
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 {};
+ return handleAddGlobalCurrencyExchange(wex, req);
}
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 {};
+ return handleRemoveGlobalCurrencyExchange(wex, req);
}
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 {};
+ return handleAddGlobalCurrencyAuditor(wex, req);
}
case WalletApiOperation.TestingWaitTasksDone: {
await waitTasksDone(wex);
@@ -1434,23 +1838,7 @@ async function dispatchRequestInternal(
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 {};
+ return await handleRemoveGlobalCurrencyAuditor(wex, req);
}
case WalletApiOperation.ImportDb: {
const req = codecForImportDbRequest().decode(payload);
@@ -1495,11 +1883,11 @@ async function dispatchRequestInternal(
return {};
}
case WalletApiOperation.Shutdown: {
- wex.ws.stop();
- return {};
+ const req = codecForEmptyObject().decode(payload);
+ return await handleShutdown(wex, req);
}
case WalletApiOperation.GetVersion: {
- return getVersion(wex);
+ return handleGetVersion(wex);
}
case WalletApiOperation.TestingWaitTransactionsFinal:
return await waitUntilAllTransactionsFinal(wex);
@@ -1507,14 +1895,11 @@ async function dispatchRequestInternal(
return await waitUntilRefreshesDone(wex);
case WalletApiOperation.TestingSetTimetravel: {
const req = codecForTestingSetTimetravelRequest().decode(payload);
- setDangerousTimetravel(req.offsetMs);
- await wex.taskScheduler.reload();
- return {};
+ return await handleTestingSetTimetravel(wex, req);
}
case WalletApiOperation.DeleteExchange: {
const req = codecForDeleteExchangeRequest().decode(payload);
- await deleteExchange(wex, req);
- return {};
+ return await handleDeleteExchange(wex, req);
}
case WalletApiOperation.GetExchangeResources: {
const req = codecForGetExchangeResourcesRequest().decode(payload);
@@ -1522,45 +1907,8 @@ async function dispatchRequestInternal(
}
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);
- }
+ return handleCanonicalizeBaseUrl(wex, req);
}
- // default:
- // assertUnreachable(operation);
}
throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
@@ -1571,7 +1919,66 @@ async function dispatchRequestInternal(
);
}
-export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
+export 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;
+}
+
+function handleGetVersion(wex: WalletExecutionContext): WalletCoreVersion {
const result: WalletCoreVersion = {
implementationSemver: walletCoreBuildInfo.implementationSemver,
implementationGitHash: walletCoreBuildInfo.implementationGitHash,
@@ -1580,9 +1987,9 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
- bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
- bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
+ bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION,
devMode: wex.ws.config.testing.devModeActive,
};
return result;
@@ -1593,11 +2000,12 @@ export function getObservedWalletExecutionContext(
cancellationToken: CancellationToken,
oc: ObservabilityContext,
): WalletExecutionContext {
+ const db = ws.createDbAccessHandle(cancellationToken);
const wex: WalletExecutionContext = {
ws,
cancellationToken,
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,
@@ -1610,11 +2018,12 @@ export function getNormalWalletExecutionContext(
cancellationToken: CancellationToken,
oc: ObservabilityContext,
): WalletExecutionContext {
+ const db = ws.createDbAccessHandle(cancellationToken);
const wex: WalletExecutionContext = {
ws,
cancellationToken,
cryptoApi: ws.cryptoApi,
- db: ws.db,
+ db,
get http() {
if (ws.initCalled) {
return ws.http;
@@ -1861,6 +2270,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 +2287,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 +2331,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 +2348,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..3e2f4411f 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -64,6 +64,7 @@ import {
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
+ TalerUriAction,
Transaction,
TransactionAction,
TransactionIdStr,
@@ -95,6 +96,7 @@ import {
getRandomBytes,
j2s,
makeErrorDetail,
+ parseTalerUri,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
@@ -118,6 +120,7 @@ import {
genericWaitForState,
makeCoinAvailable,
makeCoinsVisible,
+ requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
import {
@@ -162,10 +165,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";
/**
@@ -826,12 +826,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
@@ -1100,6 +1107,10 @@ enum ExchangeAmlStatus {
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,
@@ -1124,6 +1135,7 @@ async function handleKycRequired(
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,
@@ -1463,7 +1475,6 @@ async function processPlanchetVerifyAndStoreCoin(
sourceTransactionId: transactionId,
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
ageCommitmentProof: planchet.ageCommitmentProof,
- spendAllocation: undefined,
};
const planchetCoinPub = planchet.coinPub;
@@ -1616,14 +1627,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}`);
@@ -1762,12 +1778,20 @@ async function processWithdrawalGroupPendingKyc(
`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 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,
+ });
+ },
+ );
+
logger.info(`kyc long-polling response status: HTTP ${kycStatusRes.status}`);
if (
kycStatusRes.status === HttpStatusCode.Ok ||
@@ -2158,6 +2182,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"] },
@@ -2268,14 +2296,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) {
@@ -2340,20 +2360,10 @@ export interface GetWithdrawalDetailsForUriOpts {
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,
@@ -2403,6 +2413,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,
@@ -3039,8 +3066,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 +3093,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 +3112,13 @@ export async function prepareBankIntegratedWithdrawal(
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
currency: withdrawInfo.currency,
+ externalConfirmation,
},
},
reserveStatus: WithdrawalGroupStatus.DialogProposed,
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
-
const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
wex.taskScheduler.startShepherdTask(ctx.taskId);
@@ -3121,6 +3159,7 @@ export async function confirmWithdrawal(
}
const exchange = await fetchFreshExchange(wex, selectedExchange);
+ requireExchangeTosAcceptedOrThrow(exchange);
const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
const confirmUrl = withdrawalGroup.wgInfo.bankInfo.confirmUrl;
@@ -3195,22 +3234,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: {
@@ -3287,14 +3318,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}`);
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index fe64396fb..dbdb23673 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.12.2",
"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..01248e964 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.12.2",
"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.12.2"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 90679cfdd..5c622da70 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.12.2",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
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..c5f716c76 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, position: "fixed", width: "100%", height: "100%" }}>
<FullSize onClick={onClose?.onClick}>
<div
onClick={(e) => e.stopPropagation()}
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..f3172a741 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" }}>
@@ -206,20 +205,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..c0bc5532b 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;
@@ -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
@@ -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,15 @@ 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") {
+ chrome.tabs.getCurrent().then((d) => {
+ currentTab = d;
+ });
+}
+
export function ObservabilityEventsTable(): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
@@ -728,11 +735,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 +756,7 @@ export function ObservabilityEventsTable(): VNode {
};
}
return periodicRefresh();
- }, [filter]);
+ }, [filter, onlyThisScreen]);
return (
<div>
@@ -754,6 +767,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 +782,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 +808,7 @@ export function ObservabilityEventsTable(): VNode {
)}
{notifications.map((not) => {
return (
- <details key={not.id}>
+ <details key={not.groupId}>
<summary>
<div
style={{
@@ -829,10 +853,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 +963,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 +1036,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 ? (
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
index fd3fb52f8..1ca7481be 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -47,6 +47,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
index daa3ee76d..baaa9a3dd 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -34,6 +34,7 @@ export function useComponentState({
}: Props): RecursiveState<State> {
const amount = Amounts.parseOrThrow(amountStr);
const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListExchanges, {}),
@@ -49,6 +50,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`,
@@ -103,6 +109,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 status`,
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
index f0cd63fbe..dcb1f827b 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
@@ -50,6 +50,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..539ca207c 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
@@ -14,7 +14,11 @@
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,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
@@ -39,6 +43,7 @@ export namespace State {
export interface LoadingUriError {
status: "error";
+ retry: ButtonHandler;
error: ErrorAlert;
}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
index f092801ed..f15d48c23 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -57,6 +57,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 max amount to transfer`,
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..418fef505 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -18,7 +18,7 @@ import {
AmountJson,
AmountString,
CurrencySpecification,
- ExchangeListItem
+ ExchangeListItem,
} from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js";
import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
@@ -85,7 +85,7 @@ export namespace State {
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;
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index f8e27e688..0541bbf3f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -391,7 +391,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 +402,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 +417,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 +430,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 +478,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(
@@ -544,6 +544,7 @@ function exchangeSelectionState(
editableExchange: wInfo.editableExchange,
currentExchange,
toBeReceived,
+ toBeSent,
chooseCurrencies,
bankFee,
selectedCurrency,
@@ -551,7 +552,6 @@ function exchangeSelectionState(
setSelectedCurrency(s);
},
conversionInfo,
- withdrawalFee,
amount: {
value: choosenAmount,
onInput: wInfo.editableAmount
@@ -565,11 +565,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..b6a356de8 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -39,6 +39,7 @@ import {
getAmountWithFee,
} from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { Amounts } from "@gnu-taler/taler-util";
export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
const { i18n } = useTranslationContext();
@@ -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} />
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index bc66f2136..959ff4007 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-06-24 06:32+0000\n"
+"Last-Translator: Stefan Kügel <stefan.kuegel@taler.net>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/de/>\n"
"Language: de\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.4.3\n"
+"X-Generator: Weblate 5.5.5\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -1735,8 +1735,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
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..276d464a0 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -53,7 +53,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels ,
+ listenToAllChannels,
registerReloadOnNewVersion,
sendMessageToAllChannels,
openNewURLFromPopup,
@@ -276,18 +276,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 +311,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 +384,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 +492,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 +524,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 +769,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/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
index 3b7cbcbb7..5e781121b 100644
--- a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -14,7 +14,11 @@
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";
/**
@@ -51,8 +55,6 @@ const rootElementIsHTML =
// "meta[name=taler-support]",
// );
-
-
function validateTalerUri(uri: string): boolean {
return (
!!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
@@ -61,7 +63,9 @@ 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(
+ `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`,
+ ),
);
return url.href;
}
@@ -75,7 +79,7 @@ const shouldNotInject =
!rootElementIsHTML;
const logger = {
- debug: (...msg: any[]) => { },
+ debug: (...msg: any[]) => {},
info: (...msg: any[]) =>
console.log(`${new Date().toISOString()} TALER`, ...msg),
error: (...msg: any[]) =>
@@ -87,7 +91,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 +102,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 +135,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 +183,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 +231,7 @@ async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
return true;
});
} catch (e) {
- console.log(e)
+ console.log(e);
}
});
}
@@ -240,71 +253,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 +333,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 +368,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/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 8f23c0685..9feb03714 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -22,7 +22,7 @@ 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";
@@ -112,21 +112,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;
},
@@ -351,7 +351,7 @@ export function DeveloperPage(): VNode {
<a
href={new URL(`/keys`, e.exchangeBaseUrl).href}
target="_blank"
- rel="noreferrer"
+ rel="noreferrer"
>
{e.exchangeBaseUrl}
</a>
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 1f0293352..339ded173 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -1416,9 +1416,11 @@ export function TransferPickupDetails({
export function WithdrawDetails({
conversion,
amount,
+ bankFee,
}: {
conversion?: AmountJson;
amount: AmountWithFee;
+ bankFee?: AmountJson;
}): VNode {
const { i18n } = useTranslationContext();
@@ -1481,6 +1483,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>
);
}
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..ab3c465c4 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,
@@ -516,10 +519,10 @@ 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
await afterWalletIsInitialized;
- const result = await dispatch(message);
+ const result = await dispatch(message, from);
return result;
});
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index c6bf20160..fe8a4a3f7 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.12.2",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
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/utils/request.ts b/packages/web-util/src/utils/request.ts
index 23d3af468..944e65945 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -51,6 +51,7 @@ export async function defaultRequestHandler<T>(
`${options.basicAuth.username}:${options.basicAuth.password}`,
)}`;
}
+
requestHeaders["Content-Type"] =
!options.contentType || options.contentType === "json" ? "application/json" : "text/plain";